yourjs-box 1.5.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.
@@ -1,20 +1,20 @@
1
- /*! yourjs-box v1.5.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
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
5
5
  * @type {string}
6
6
  */
7
- const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
7
+ const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.history-panel{max-width:min(420px,calc(100% - 12px));width:360px}.history-title{color:var(--muted-text);font-size:11px;font-weight:600;letter-spacing:.05em;padding:4px 8px 6px;text-transform:uppercase}.history-item{justify-content:space-between}.history-label{font-family:var(--console-font);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-detail{color:var(--muted-text);flex-shrink:0;font-size:11px}.history-empty,.history-hint{color:var(--muted-text);font-size:12px;padding:6px 8px}.history-hint{border-top:1px solid var(--toolbar-border);margin-top:4px}.value-menu{bottom:auto;min-width:220px;position:fixed;right:auto;z-index:850}.toast{animation:fade-in .15s ease-out;background-color:var(--console-text);border-radius:6px;bottom:44px;box-shadow:0 6px 20px rgb(0 0 0 / .25);color:var(--console-bg);font-size:12px;left:50%;max-width:calc(100% - 32px);padding:6px 12px;pointer-events:none;position:fixed;transform:translateX(-50%);z-index:860}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
8
8
  /**
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.5.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.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&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :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=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"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>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><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.ts,.mts,.cts,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism :language=\"language\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" :language=\"language\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Language</dt><dd>{{ isTypeScript ? 'TypeScript (types are removed by Babel, not checked)' : 'JavaScript' }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with {{ builtWith }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
18
18
  /**
19
19
  * The URL of this script which is used to remove this script's lines from the
20
20
  * stack traces of errors in window mode.
@@ -33,6 +33,8 @@
33
33
  * was tested with.
34
34
  */
35
35
  const LIBRARY_VERSIONS = {
36
+ // Only loaded by TypeScript consoles.
37
+ '@babel/standalone': '7.29.9',
36
38
  'ace-builds': '1.44.0',
37
39
  'acorn': '8.18.0',
38
40
  'prism-themes': '1.9.0',
@@ -89,8 +91,8 @@
89
91
  * to these calls.
90
92
  */
91
93
  const RELAYABLE_FUNCS = {
92
- viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
93
- runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
94
+ viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'onStoredAsGlobal', 'onValueJson', 'updateDescriptionFor'],
95
+ runner: ['clearLogs', 'getValueAsJson', 'refreshDescription', 'reset', 'runCode', 'sendDescriptionFor', 'storeAsGlobal'],
94
96
  // Things that the viewer (but never the runner) can ask this script to do.
95
97
  host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
96
98
  };
@@ -138,6 +140,9 @@
138
140
  // How wrapping the last expression of each block shifted the columns on its
139
141
  // line so that stack traces can show the original columns.
140
142
  const columnShiftsBySnippet = {};
143
+ // For TypeScript, where each position in the code that ran came from in the
144
+ // TypeScript code (see runCode()).
145
+ const sourceMapsBySnippet = {};
141
146
 
142
147
  const counts = {};
143
148
  const timers = {};
@@ -334,15 +339,40 @@
334
339
  .split('\n')
335
340
  .filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
336
341
  .join('\n')
337
- // Undoes the shift in columns caused by wrapping the last expression.
338
- .replace(/(snippet-\d+\.js):(\d+):(\d+)/g, (match, name, line, column) => {
342
+ .replace(/(snippet-\d+\.[jt]s):(\d+):(\d+)/g, (match, name, line, column) => {
343
+ line = +line;
344
+ column = +column;
345
+ // Undoes the shift in columns caused by wrapping the last expression.
339
346
  const info = columnShiftsBySnippet[name];
340
- return info && +line === info.line && +column > info.column
341
- ? `${name}:${line}:${Math.max(info.column, column - info.shift)}`
342
- : match;
347
+ if (info && line === info.line && column > info.column) {
348
+ column = Math.max(info.column, column - info.shift);
349
+ }
350
+ [line, column] = toSourcePosition(sourceMapsBySnippet[name], line, column);
351
+ return `${name}:${line}:${column}`;
343
352
  });
344
353
  }
345
354
 
355
+ /**
356
+ * Finds where a position in compiled code came from in the original code.
357
+ * @param {number[][][]=} sourceMap
358
+ * For each line of the compiled code, its mappings as [compiled column,
359
+ * original line, original column] (all starting at 0) sorted by column.
360
+ * @param {number} line
361
+ * @param {number} column
362
+ * @returns {[number, number]}
363
+ * The original line and column (both starting at 1) or the given ones if
364
+ * the position isn't mapped.
365
+ */
366
+ function toSourcePosition(sourceMap, line, column) {
367
+ const mappings = sourceMap?.[line - 1];
368
+ let best;
369
+ for (const mapping of mappings ?? []) {
370
+ if (mapping[0] > column - 1) break;
371
+ best = mapping;
372
+ }
373
+ return best ? [best[1] + 1, best[2] + 1 + (column - 1 - best[0])] : [line, column];
374
+ }
375
+
346
376
  function without(props, obj) {
347
377
  if (!obj) return obj => without(props, obj);
348
378
  const ret = {...obj};
@@ -631,9 +661,9 @@
631
661
  const $entries = [];
632
662
  const protoEntries = [];
633
663
  const $protoEntries = [];
664
+ let wasIterated = false;
634
665
 
635
666
  if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
636
- let wasIterated = false;
637
667
  try {
638
668
  // Map
639
669
  if (typeName === 'Map') {
@@ -681,7 +711,9 @@
681
711
  }
682
712
  }
683
713
 
684
- return {entries, protoEntries, $entries, $protoEntries};
714
+ // Map and Set entries (and other iterated values) aren't properties so
715
+ // they have no property path.
716
+ return {entries, protoEntries, entriesArePropertyKeys: !wasIterated, $entries, $protoEntries};
685
717
  }
686
718
 
687
719
  // ['123', 0, 'entries', 0]
@@ -707,12 +739,22 @@
707
739
  * @param {[number, number]=} runOptions.resultRange
708
740
  * The start and end index of the last expression in the code whose value
709
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.
710
751
  */
711
752
  async function runCode(jsCode, runOptions) {
712
- const {blockType, resultRange} = Object(runOptions);
753
+ const {blockType, resultRange, language, sourceMap, compileError} = Object(runOptions);
713
754
 
714
755
  // Names the code so that stack traces refer to it by this name.
715
- const snippetName = `snippet-${++snippetCount}.js`;
756
+ const snippetName = `snippet-${++snippetCount}.${language === 'typescript' ? 'ts' : 'js'}`;
757
+ if (Array.isArray(sourceMap)) sourceMapsBySnippet[snippetName] = sourceMap;
716
758
 
717
759
  // Wraps the last expression so that its value is reported. This keeps
718
760
  // everything on the same lines so that line numbers in errors still match
@@ -733,7 +775,17 @@
733
775
 
734
776
  runningBlockType = blockType === 'module' ? 'module' : 'classic';
735
777
  try {
736
- if (blockType === 'module') {
778
+ if (compileError) {
779
+ // Reported here so that it shows up after anything logged by the code
780
+ // that ran before it.
781
+ const {message, line, column} = compileError;
782
+ reportUncaught({
783
+ name: 'SyntaxError',
784
+ message: `${message}`,
785
+ stack: `SyntaxError: ${message}\n at ${snippetName}:${+line}:${+column}`,
786
+ });
787
+ }
788
+ else if (blockType === 'module') {
737
789
  // Waits for the module to finish (including any top-level await).
738
790
  await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
739
791
  }
@@ -792,16 +844,25 @@
792
844
  * entry indices.
793
845
  */
794
846
  function sendDescriptionFor(path) {
795
- // Get the description of the desired value.
796
- let level = logArgsById;
797
- let pathPartIndex = 0;
798
- for (let pathPart of path) {
799
- if (pathPartIndex && pathPartIndex % 2 === 0) {
800
- pathPart = '$' + pathPart;
801
- }
802
- level = level?.[pathPart];
803
- pathPartIndex++;
804
- }
847
+ sendDescription(path, false);
848
+ }
849
+
850
+ /**
851
+ * Describes the value again (eg. after the code changed it) including its
852
+ * preview.
853
+ * @param {(string|number)[]} path
854
+ */
855
+ function refreshDescription(path) {
856
+ sendDescription(path, true);
857
+ }
858
+
859
+ /**
860
+ * @param {(string|number)[]} path
861
+ * @param {boolean} isRefresh
862
+ * If `true` the value's preview is included too.
863
+ */
864
+ function sendDescription(path, isRefresh) {
865
+ const level = getLevel(path);
805
866
  if (!level) return;
806
867
  const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
807
868
 
@@ -809,13 +870,97 @@
809
870
  Object.assign(level, description);
810
871
 
811
872
  // Send the description without values back to the viewer.
873
+ const summary = isRefresh
874
+ ? summarize(level.value, path.length > 2 ? 1 : 0, path[path.length - 2] === 'protoEntries')
875
+ : {};
812
876
  send({
813
877
  target: 'viewer',
814
878
  func: 'updateDescriptionFor',
815
- args: [path, without(['$entries', '$protoEntries'], description)],
879
+ args: [path, {...without(['$entries', '$protoEntries'], description), ...summary}],
816
880
  });
817
881
  }
818
882
 
883
+ /**
884
+ * Finds what is stored for a logged value (or a value within it).
885
+ * @param {(string|number)[]} path
886
+ * The log ID, the argument index and then pairs of entry group keys and
887
+ * entry indices.
888
+ * @returns {{value: *, receiver?: *}|undefined}
889
+ */
890
+ function getLevel(path) {
891
+ let level = logArgsById;
892
+ path.forEach((pathPart, index) => {
893
+ level = level?.[index && index % 2 === 0 ? '$' + pathPart : pathPart];
894
+ });
895
+ return level;
896
+ }
897
+
898
+ /**
899
+ * Sends a value as JSON to the viewer (eg. to copy or save it).
900
+ * @param {(string|number)[]} path
901
+ * @param {string} requestId
902
+ */
903
+ function getValueAsJson(path, requestId) {
904
+ const level = getLevel(path);
905
+ let result;
906
+ try {
907
+ result = level ? toJson(level.value) : {error: 'The value is no longer available.'};
908
+ }
909
+ catch (e) {
910
+ result = {error: `The value couldn't be converted to JSON (${e?.message ?? e}).`};
911
+ }
912
+ send({target: 'viewer', func: 'onValueJson', args: [{requestId, ...result}]});
913
+ }
914
+
915
+ /**
916
+ * Converts a value to JSON like JSON.stringify() does except that Maps and
917
+ * Sets become arrays, BigInts become strings and circular references (an
918
+ * object inside of itself) are left out instead of causing an error. An
919
+ * object that appears more than once without being circular is kept.
920
+ * @param {*} value
921
+ * @returns {{json?: string, circularCount: number, error?: string}}
922
+ */
923
+ function toJson(value) {
924
+ let circularCount = 0;
925
+ // The objects currently being converted along with what they were
926
+ // converted from (eg. a Map is converted to an array).
927
+ const stack = [];
928
+ const json = JSON.stringify(value, function(key, val) {
929
+ // `this` is the object containing `key` so anything after it on the
930
+ // stack has already been converted.
931
+ while (stack.length && stack[stack.length - 1].converted !== this) stack.pop();
932
+
933
+ if ('bigint' === typeof val) return `${val}`;
934
+ if (val !== null && 'object' === typeof val) {
935
+ const original = val;
936
+ if (stack.some(item => item.original === original)) {
937
+ circularCount++;
938
+ return undefined;
939
+ }
940
+ if (val instanceof Map || val instanceof Set) val = [...val];
941
+ stack.push({original, converted: val});
942
+ }
943
+ return val;
944
+ }, 2);
945
+ return json === undefined
946
+ ? {error: 'This value can\'t be represented as JSON.', circularCount}
947
+ : {json, circularCount};
948
+ }
949
+
950
+ /**
951
+ * Stores a value as a global variable (temp1, temp2, etc.) like the
952
+ * browser's console does so that code can use it.
953
+ * @param {(string|number)[]} path
954
+ */
955
+ function storeAsGlobal(path) {
956
+ const level = getLevel(path);
957
+ if (!level) return;
958
+ let index = 1;
959
+ while (`temp${index}` in globalThis) index++;
960
+ globalThis[`temp${index}`] = level.value;
961
+ send({target: 'viewer', func: 'onStoredAsGlobal', args: [`temp${index}`]});
962
+ }
963
+
819
964
  /**
820
965
  * Forgets all of the logged values (eg. when the console is cleared).
821
966
  */
@@ -824,7 +969,7 @@
824
969
  groupIds = [];
825
970
  }
826
971
 
827
- return {clearLogs, destroy, runCode, sendDescriptionFor};
972
+ return {clearLogs, destroy, getValueAsJson, refreshDescription, runCode, sendDescriptionFor, storeAsGlobal};
828
973
  }
829
974
 
830
975
 
@@ -950,6 +1095,7 @@
950
1095
  function createConsole({code, dataset, height, insert}) {
951
1096
  const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
952
1097
  const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
1098
+ const language = dataset.language === 'typescript' ? 'typescript' : 'javascript';
953
1099
  const showResults = dataset.showResults !== 'false';
954
1100
  // An empty data-imports-url turns off loading packages by name.
955
1101
  const importsUrl = dataset.importsUrl == null
@@ -1141,6 +1287,12 @@
1141
1287
  */
1142
1288
  function init(jsCode, dataset, meta) {
1143
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);
1144
1296
 
1145
1297
  const hidePrefix = dataset.hidePrefix ?? '';
1146
1298
  const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
@@ -1164,6 +1316,7 @@
1164
1316
  // The name of the file that was last opened or saved.
1165
1317
  fileName: null,
1166
1318
  blockType: meta.blockType,
1319
+ language: meta.language,
1167
1320
  showResults: meta.showResults,
1168
1321
  packageInfo: meta.packageInfo,
1169
1322
  libraryVersions: meta.libraryVersions,
@@ -1199,6 +1352,18 @@
1199
1352
  dividerSize: '8px',
1200
1353
  tempDividerPct: null,
1201
1354
  isMenuOpen: false,
1355
+ isHistoryOpen: false,
1356
+ // The code that was run (newest last) which can be brought back into
1357
+ // the editor like the browser's console (eg. with the up arrow).
1358
+ commandHistory: [],
1359
+ // While going through the history: the index of the code shown and
1360
+ // the unrun code that was in the editor (shown again at the end).
1361
+ historyIndex: null,
1362
+ historyDraft: null,
1363
+ /** @type {{x: number, y: number, path: any[], propertyPath: string?, isPrimitive: boolean}?} */
1364
+ valueMenu: null,
1365
+ /** @type {{message: string}?} */
1366
+ toast: null,
1202
1367
  textScale: loadTextScale(),
1203
1368
  isFullscreen: false,
1204
1369
  // Set when full screen isn't allowed so the console fills the page.
@@ -1227,6 +1392,21 @@
1227
1392
  const name = this.theme === 'dark' ? 'Dark' : 'Light';
1228
1393
  return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
1229
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
+ },
1230
1410
  blockTypeDescription() {
1231
1411
  return this.blockType === 'module'
1232
1412
  ? 'Modules (top-level await and imports work, declarations stay in each block)'
@@ -1321,11 +1501,23 @@
1321
1501
  bottomButtons() {
1322
1502
  const isFullscreen = this.isFullscreen || this.isMaximized;
1323
1503
  return [
1504
+ {
1505
+ iconName: 'more',
1506
+ title: 'More',
1507
+ className: 'more-button',
1508
+ callback() { this.toggleMenu(); },
1509
+ },
1324
1510
  {
1325
1511
  iconName: 'clear',
1326
1512
  title: 'Clear console',
1327
1513
  callback() { this.clearConsole(); },
1328
1514
  },
1515
+ {
1516
+ iconName: 'history',
1517
+ title: `History (${isMac ? '\u2325' : 'Alt+'}H)`,
1518
+ className: 'history-button',
1519
+ callback() { this.toggleHistory(); },
1520
+ },
1329
1521
  {
1330
1522
  iconName: 'horizontalView',
1331
1523
  title: 'Show the editor below the console',
@@ -1343,12 +1535,6 @@
1343
1535
  title: isFullscreen ? 'Exit full screen' : 'Full screen',
1344
1536
  callback() { this.toggleFullscreen(); },
1345
1537
  },
1346
- {
1347
- iconName: 'more',
1348
- title: 'More',
1349
- className: 'more-button',
1350
- callback() { this.toggleMenu(); },
1351
- },
1352
1538
  {
1353
1539
  iconName: this.runningCount ? 'spinner' : 'play',
1354
1540
  label: 'Run',
@@ -1359,6 +1545,20 @@
1359
1545
  },
1360
1546
  ].filter(btn => !btn.showIf || btn.showIf.call(this));
1361
1547
  },
1548
+ /**
1549
+ * The history's entries (newest first) for the history list.
1550
+ */
1551
+ historyEntries() {
1552
+ return this.commandHistory.map((code, index) => {
1553
+ const [group] = parseJSCodeGroups(code);
1554
+ const lines = (group?.lines ?? code).split('\n').filter(line => line.trim());
1555
+ return {
1556
+ index,
1557
+ label: group?.headerLines || lines[0] || '',
1558
+ detail: `${lines.length} line${lines.length === 1 ? '' : 's'}`,
1559
+ };
1560
+ }).reverse();
1561
+ },
1362
1562
  canShrinkText() {
1363
1563
  return this.textScale > TEXT_SCALES[0];
1364
1564
  },
@@ -1399,9 +1599,110 @@
1399
1599
  this.runGroup(jsCodeGroup0);
1400
1600
 
1401
1601
  // Remove the code that was run from the editor.
1402
- this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1403
-
1404
- this.runCount++;
1602
+ const remainingCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1603
+
1604
+ if (this.historyIndex !== null) {
1605
+ // Code from the history was run so the unrun code comes back. It
1606
+ // doesn't count as moving through the code so hidden blocks still
1607
+ // run in their places.
1608
+ this.jsCode = [remainingCode, this.historyDraft].filter(code => code.trim()).join('\n');
1609
+ this.historyIndex = this.historyDraft = null;
1610
+ }
1611
+ else {
1612
+ this.jsCode = remainingCode;
1613
+ this.runCount++;
1614
+ }
1615
+ },
1616
+ /**
1617
+ * Shows older (-1) or newer (1) code from the history in the editor.
1618
+ * Going newer than the newest shows the unrun code again.
1619
+ * @param {-1|1} direction
1620
+ * @returns {boolean}
1621
+ * `true` if the history was used (so the key shouldn't do anything
1622
+ * else).
1623
+ */
1624
+ navigateHistory(direction) {
1625
+ const history = this.commandHistory;
1626
+ if (direction < 0) {
1627
+ if (!history.length) return false;
1628
+ if (this.historyIndex === null) {
1629
+ this.historyDraft = this.jsCode;
1630
+ this.historyIndex = history.length;
1631
+ }
1632
+ if (this.historyIndex > 0) this.showHistoryCode(this.historyIndex - 1, 'start');
1633
+ return true;
1634
+ }
1635
+ if (this.historyIndex === null) return false;
1636
+ if (this.historyIndex < history.length - 1) {
1637
+ this.showHistoryCode(this.historyIndex + 1, 'end');
1638
+ }
1639
+ else {
1640
+ const draft = this.historyDraft;
1641
+ this.historyIndex = this.historyDraft = null;
1642
+ this.setEditorCode(draft, 'end');
1643
+ }
1644
+ return true;
1645
+ },
1646
+ /**
1647
+ * Shows code from the history in the editor (saving the unrun code if
1648
+ * this is the first time).
1649
+ * @param {number} index
1650
+ * @param {"start"|"end"} cursorAt
1651
+ */
1652
+ showHistoryCode(index, cursorAt) {
1653
+ if (this.historyIndex === null) this.historyDraft = this.jsCode;
1654
+ this.historyIndex = index;
1655
+ this.setEditorCode(this.commandHistory[index], cursorAt);
1656
+ },
1657
+ /**
1658
+ * @param {string} code
1659
+ * @param {"start"|"end"} cursorAt
1660
+ * Where the cursor goes so that pressing the same arrow key again
1661
+ * keeps going through the history.
1662
+ */
1663
+ setEditorCode(code, cursorAt) {
1664
+ this.jsCode = code;
1665
+ this.$nextTick(() => {
1666
+ const editor = this.$refs.editor?.editor;
1667
+ if (!editor) return;
1668
+ if (cursorAt === 'end') editor.navigateFileEnd();
1669
+ else editor.navigateFileStart();
1670
+ });
1671
+ },
1672
+ /**
1673
+ * Called by the editor when the up or down arrow is pressed with the
1674
+ * cursor at the very start or end (and nothing selected).
1675
+ * @param {{direction: -1|1, handled: boolean}} event
1676
+ */
1677
+ onEditorHistoryKey(event) {
1678
+ event.handled = this.navigateHistory(event.direction);
1679
+ },
1680
+ toggleHistory() {
1681
+ if (this.isHistoryOpen) {
1682
+ this.isHistoryOpen = false;
1683
+ }
1684
+ else {
1685
+ this.closeMenu();
1686
+ this.closeValueMenu();
1687
+ this.isHistoryOpen = true;
1688
+ this.$nextTick(() => this.$refs.historyPanel?.querySelector('.menu-item')?.focus());
1689
+ }
1690
+ },
1691
+ /**
1692
+ * Puts code from the history list into the editor.
1693
+ * @param {number} index
1694
+ */
1695
+ recallHistory(index) {
1696
+ this.isHistoryOpen = false;
1697
+ this.showHistoryCode(index, 'start');
1698
+ this.$nextTick(() => this.$refs.editor?.editor.focus());
1699
+ },
1700
+ /**
1701
+ * Forgets that the history was being gone through (eg. when the code in
1702
+ * the editor is replaced).
1703
+ */
1704
+ stopNavigatingHistory() {
1705
+ this.historyIndex = this.historyDraft = null;
1405
1706
  },
1406
1707
  /**
1407
1708
  * Runs the next hidden group if all of the visible code that came
@@ -1429,17 +1730,35 @@
1429
1730
  });
1430
1731
  this.runningCount++;
1431
1732
  this.runHistory.push(group.allLines);
1432
- const {code, resultRange} = prepareCode(group.lines, {
1433
- blockType: this.blockType,
1434
- importsUrl: this.importsUrl,
1435
- // Like the browser's console, show the value of the last
1436
- // expression (except for hidden code).
1437
- findResult: this.showResults && !isHidden,
1438
- });
1733
+ // Hidden code wasn't typed or seen so it isn't in the history.
1734
+ const historyCode = group.allLines.trim();
1735
+ if (!isHidden && historyCode && this.commandHistory[this.commandHistory.length - 1] !== historyCode) {
1736
+ this.commandHistory.push(historyCode);
1737
+ if (this.commandHistory.length > 200) this.commandHistory.shift();
1738
+ }
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
+ });
1439
1752
  messageParent({
1440
1753
  target: 'runner',
1441
1754
  func: 'runCode',
1442
- args: [code, {blockType: this.blockType, resultRange}],
1755
+ args: [code, {
1756
+ blockType,
1757
+ resultRange,
1758
+ language,
1759
+ sourceMap: compiled.sourceMap,
1760
+ compileError: compiled.error,
1761
+ }],
1443
1762
  });
1444
1763
  },
1445
1764
  clearConsole() {
@@ -1470,6 +1789,7 @@
1470
1789
  this.runCount = 0;
1471
1790
  this.runningCount = 0;
1472
1791
  this.runHistory = [];
1792
+ this.stopNavigatingHistory();
1473
1793
  messageParent({target: 'runner', func: 'reset', args: []});
1474
1794
  this.runHiddenGroups();
1475
1795
  },
@@ -1512,14 +1832,17 @@
1512
1832
  async saveFile() {
1513
1833
  this.closeMenu();
1514
1834
  const code = this.currentCode + '\n';
1515
- 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']}};
1516
1839
 
1517
1840
  // Shows a "Save as" dialog where it is supported.
1518
1841
  if ('function' === typeof window.showSaveFilePicker) {
1519
1842
  try {
1520
1843
  const handle = await showSaveFilePicker({
1521
1844
  suggestedName,
1522
- types: [{description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}}],
1845
+ types: [fileType],
1523
1846
  });
1524
1847
  const writable = await handle.createWritable();
1525
1848
  await writable.write(code);
@@ -1533,7 +1856,7 @@
1533
1856
  }
1534
1857
  }
1535
1858
 
1536
- const url = URL.createObjectURL(new Blob([code], {type: 'text/javascript'}));
1859
+ const url = URL.createObjectURL(new Blob([code], {type: Object.keys(fileType.accept)[0]}));
1537
1860
  Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
1538
1861
  setTimeout(() => URL.revokeObjectURL(url), 1000);
1539
1862
  },
@@ -1552,6 +1875,7 @@
1552
1875
  });
1553
1876
  if (!isConfirmed) return;
1554
1877
  }
1878
+ this.stopNavigatingHistory();
1555
1879
  this.jsCode = code;
1556
1880
  },
1557
1881
  /**
@@ -1637,6 +1961,9 @@
1637
1961
  jsCode: this.jsCode,
1638
1962
  originalCode: this.originalCode,
1639
1963
  fileName: this.fileName,
1964
+ commandHistory: this.commandHistory,
1965
+ historyIndex: this.historyIndex,
1966
+ historyDraft: this.historyDraft,
1640
1967
  runHistory: this.runHistory,
1641
1968
  hiddenGroups: this.hiddenGroups,
1642
1969
  runCount: this.runCount,
@@ -1658,6 +1985,8 @@
1658
1985
  this.closeMenu();
1659
1986
  }
1660
1987
  else {
1988
+ this.isHistoryOpen = false;
1989
+ this.closeValueMenu();
1661
1990
  this.isMenuOpen = true;
1662
1991
  this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
1663
1992
  }
@@ -1670,14 +1999,21 @@
1670
1999
  * Lets the arrow keys move between the menu's items.
1671
2000
  * @param {KeyboardEvent} evt
1672
2001
  */
2002
+ /**
2003
+ * Lets Escape close a menu (or the history list) and the arrow keys move
2004
+ * between its items.
2005
+ * @param {KeyboardEvent} evt
2006
+ */
1673
2007
  onMenuKeyDown(evt) {
1674
2008
  if (evt.key === 'Escape') {
1675
2009
  evt.stopPropagation();
1676
- this.closeMenu(true);
2010
+ this.closeMenu(this.isMenuOpen);
2011
+ this.closeValueMenu();
2012
+ this.isHistoryOpen = false;
1677
2013
  }
1678
2014
  else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
1679
2015
  evt.preventDefault();
1680
- const items = [...this.$refs.moreMenu.querySelectorAll('.menu-item:not(:disabled)')];
2016
+ const items = [...evt.currentTarget.querySelectorAll('.menu-item:not(:disabled)')];
1681
2017
  const index = items.indexOf(document.activeElement);
1682
2018
  const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
1683
2019
  next?.focus();
@@ -1757,15 +2093,189 @@
1757
2093
  if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
1758
2094
  this.closeMenu();
1759
2095
  }
2096
+ if (this.valueMenu && !evt.target.closest('.value-menu')) {
2097
+ this.closeValueMenu();
2098
+ }
2099
+ if (this.isHistoryOpen && !evt.target.closest('.history-panel, .history-button')) {
2100
+ this.isHistoryOpen = false;
2101
+ }
1760
2102
  },
1761
2103
  /**
1762
2104
  * @param {KeyboardEvent} evt
1763
2105
  */
1764
2106
  onWindowKeyDown(evt) {
2107
+ if (evt.altKey && !evt.ctrlKey && !evt.metaKey && evt.code === 'KeyH') {
2108
+ evt.preventDefault();
2109
+ evt.stopPropagation();
2110
+ this.toggleHistory();
2111
+ return;
2112
+ }
2113
+ if (evt.key === 'Escape' && this.valueMenu) {
2114
+ this.closeValueMenu();
2115
+ return;
2116
+ }
1765
2117
  if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
1766
2118
  this.setMaximized(false);
1767
2119
  }
1768
2120
  },
2121
+ /**
2122
+ * Shows the menu for a value that was right-clicked.
2123
+ * @param {MouseEvent} evt
2124
+ * @param {{path: any[], keyPath: string[]?, isPrimitive: boolean}} value
2125
+ */
2126
+ openValueMenu(evt, {path, keyPath, isPrimitive}) {
2127
+ this.closeMenu();
2128
+ this.valueMenu = {
2129
+ x: evt.clientX,
2130
+ y: evt.clientY,
2131
+ path: path.slice(),
2132
+ // Only properties and items (not logged values themselves) have
2133
+ // a property path.
2134
+ propertyPath: keyPath?.length ? formatPropertyPath(keyPath) : null,
2135
+ fileName: keyPath?.length && /^[\w$-]+$/.test(keyPath[keyPath.length - 1]) ? keyPath[keyPath.length - 1] : 'value',
2136
+ isPrimitive,
2137
+ };
2138
+ // Keeps the menu inside of the window.
2139
+ this.$nextTick(() => {
2140
+ const menu = this.$refs.valueMenu;
2141
+ if (!menu || !this.valueMenu) return;
2142
+ const {width, height} = menu.getBoundingClientRect();
2143
+ this.valueMenu.x = Math.max(4, Math.min(this.valueMenu.x, innerWidth - width - 4));
2144
+ this.valueMenu.y = Math.max(4, Math.min(this.valueMenu.y, innerHeight - height - 4));
2145
+ menu.querySelector('.menu-item')?.focus();
2146
+ });
2147
+ },
2148
+ closeValueMenu() {
2149
+ this.valueMenu = null;
2150
+ },
2151
+ /**
2152
+ * Asks the runner to convert the value to JSON.
2153
+ * @param {any[]} path
2154
+ * @returns {Promise<{json: string?, circularCount: number, error: string?}>}
2155
+ */
2156
+ requestValueJson(path) {
2157
+ const requestId = `${Date.now()}-${Math.random()}`;
2158
+ return new Promise(resolve => {
2159
+ jsonRequests.set(requestId, resolve);
2160
+ messageParent({target: 'runner', func: 'getValueAsJson', args: [path, requestId]});
2161
+ });
2162
+ },
2163
+ /**
2164
+ * Shows a short message (eg. "Copied") for a few seconds.
2165
+ * @param {string} message
2166
+ */
2167
+ showToast(message) {
2168
+ clearTimeout(this.toastTimeout);
2169
+ this.toast = {message};
2170
+ this.toastTimeout = setTimeout(() => this.toast = null, 3500);
2171
+ },
2172
+ /**
2173
+ * @param {{json: string?, circularCount: number, error: string?}} result
2174
+ * @param {string} action
2175
+ * What was done (eg. "Copied as JSON").
2176
+ */
2177
+ showJsonToast({circularCount, error}, action) {
2178
+ this.showToast(
2179
+ error || action + (circularCount
2180
+ ? ` (${circularCount} circular reference${circularCount === 1 ? ' was' : 's were'} left out)`
2181
+ : '')
2182
+ );
2183
+ },
2184
+ /**
2185
+ * Copies the value as JSON. The copy is started right away (while
2186
+ * the browser still allows it) with the JSON filled in when it is
2187
+ * ready.
2188
+ */
2189
+ async copyValueAsJson() {
2190
+ // A plain copy because the menu's (reactive) path can't be sent.
2191
+ const path = [...this.valueMenu.path];
2192
+ this.closeValueMenu();
2193
+ const resultPromise = this.requestValueJson(path);
2194
+ try {
2195
+ if (window.ClipboardItem && navigator.clipboard?.write) {
2196
+ await navigator.clipboard.write([new ClipboardItem({
2197
+ 'text/plain': resultPromise.then(({json, error}) => {
2198
+ if (error) throw new Error(error);
2199
+ return new Blob([json], {type: 'text/plain'});
2200
+ }),
2201
+ })]);
2202
+ }
2203
+ else {
2204
+ const {json, error} = await resultPromise;
2205
+ if (error) throw new Error(error);
2206
+ await navigator.clipboard.writeText(json);
2207
+ }
2208
+ this.showJsonToast(await resultPromise, 'Copied as JSON');
2209
+ }
2210
+ catch (e) {
2211
+ const result = await resultPromise;
2212
+ this.showToast(result.error || 'The value couldn\'t be copied. Your browser may not allow copying here.');
2213
+ }
2214
+ },
2215
+ /**
2216
+ * Saves the value as a JSON file. The "Save as" dialog is shown right
2217
+ * away (while the browser still allows it).
2218
+ */
2219
+ async saveValueAsJson() {
2220
+ const {fileName} = this.valueMenu;
2221
+ const path = [...this.valueMenu.path];
2222
+ this.closeValueMenu();
2223
+ const resultPromise = this.requestValueJson(path);
2224
+ const suggestedName = `${fileName}.json`;
2225
+ let handle;
2226
+ if ('function' === typeof window.showSaveFilePicker) {
2227
+ try {
2228
+ handle = await showSaveFilePicker({
2229
+ suggestedName,
2230
+ types: [{description: 'JSON', accept: {'application/json': ['.json']}}],
2231
+ });
2232
+ }
2233
+ catch (e) {
2234
+ if (e?.name === 'AbortError') return;
2235
+ }
2236
+ }
2237
+ const result = await resultPromise;
2238
+ if (result.error) {
2239
+ this.showToast(result.error);
2240
+ return;
2241
+ }
2242
+ const text = result.json + '\n';
2243
+ if (handle) {
2244
+ const writable = await handle.createWritable();
2245
+ await writable.write(text);
2246
+ await writable.close();
2247
+ }
2248
+ else {
2249
+ const url = URL.createObjectURL(new Blob([text], {type: 'application/json'}));
2250
+ Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
2251
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
2252
+ }
2253
+ this.showJsonToast(result, 'Saved as JSON');
2254
+ },
2255
+ storeValueAsGlobal() {
2256
+ const path = [...this.valueMenu.path];
2257
+ this.closeValueMenu();
2258
+ messageParent({target: 'runner', func: 'storeAsGlobal', args: [path]});
2259
+ },
2260
+ async copyPropertyPath() {
2261
+ const {propertyPath} = this.valueMenu;
2262
+ this.closeValueMenu();
2263
+ try {
2264
+ await navigator.clipboard.writeText(propertyPath);
2265
+ this.showToast(`Copied ${propertyPath}`);
2266
+ }
2267
+ catch (e) {
2268
+ this.showToast('The property path couldn\'t be copied. Your browser may not allow copying here.');
2269
+ }
2270
+ },
2271
+ /**
2272
+ * Shows the value as it is now (eg. after the code changed it).
2273
+ */
2274
+ refreshValue() {
2275
+ const path = [...this.valueMenu.path];
2276
+ this.closeValueMenu();
2277
+ messageParent({target: 'runner', func: 'refreshDescription', args: [path]});
2278
+ },
1769
2279
  closeDialog(isConfirmed) {
1770
2280
  const {dialog} = this;
1771
2281
  this.dialog = null;
@@ -1786,6 +2296,7 @@
1786
2296
  }
1787
2297
  },
1788
2298
  onDisplaysScroll() {
2299
+ this.closeValueMenu();
1789
2300
  const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
1790
2301
  this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
1791
2302
  },
@@ -1859,7 +2370,8 @@
1859
2370
  mounted() {
1860
2371
  addEventListener('mousemove', this.onWindowMouseMove);
1861
2372
  addEventListener('mousedown', this.onWindowMouseDown);
1862
- addEventListener('keydown', this.onWindowKeyDown);
2373
+ // Capturing lets shortcuts (eg. Alt+H) work before the editor sees them.
2374
+ addEventListener('keydown', this.onWindowKeyDown, true);
1863
2375
  addEventListener('resize', this.onWindowResize);
1864
2376
  addEventListener('mouseup', this.onWindowMouseUp);
1865
2377
  addEventListener('error', this.onWindowError);
@@ -2005,6 +2517,28 @@
2005
2517
  });
2006
2518
 
2007
2519
  // Attempt to capture key combos.
2520
+ // Like the browser's console, the up arrow at the very start (or the
2521
+ // down arrow at the very end) with nothing selected goes through the
2522
+ // history. This listens while capturing so it runs before Ace moves the
2523
+ // cursor.
2524
+ this.$refs.editor.addEventListener('keydown', evt => {
2525
+ const isUp = evt.key === 'ArrowUp';
2526
+ if (!isUp && evt.key !== 'ArrowDown') return;
2527
+ if (evt.shiftKey || evt.altKey || evt.ctrlKey || evt.metaKey || !editor.selection.isEmpty()) return;
2528
+ const {row, column} = editor.getCursorPosition();
2529
+ const lastRow = editor.session.getLength() - 1;
2530
+ const isAtEdge = isUp
2531
+ ? row === 0 && column === 0
2532
+ : row === lastRow && column === editor.session.getLine(lastRow).length;
2533
+ if (!isAtEdge) return;
2534
+ const event = {direction: isUp ? -1 : 1, handled: false};
2535
+ this.$emit('historyKey', event);
2536
+ if (event.handled) {
2537
+ evt.preventDefault();
2538
+ evt.stopPropagation();
2539
+ }
2540
+ }, true);
2541
+
2008
2542
  editor.textInput.getElement().addEventListener('keydown', (evt) => {
2009
2543
  const isComboKeyCode = evt.keyCode === 16 // SHIFT
2010
2544
  || evt.keyCode === 17 // CTRL
@@ -2064,7 +2598,7 @@
2064
2598
 
2065
2599
  codeElem.textContent = this.code;
2066
2600
 
2067
- codeElem.className = `language-javascript`;
2601
+ codeElem.className = `language-${this.language ?? 'javascript'}`;
2068
2602
  preElem.className = '';
2069
2603
 
2070
2604
  for (const propName of ['lineNumbers', 'matchBraces']) {
@@ -2109,6 +2643,10 @@
2109
2643
  // https://icon-sets.iconify.design/mdi/error-outline/
2110
2644
  error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
2111
2645
  // Icons similar to those in the browser's console.
2646
+ history: '<svg viewBox="0 0 16 16"><path d="M2.5 8a5.5 5.5 0 1 0 1.6-3.9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M2.25 2.5v2.75H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 5v3.25l2.25 1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
2647
+ copy: '<svg viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 3.5v-.75A1.25 1.25 0 0 0 9.25 1.5h-5A1.25 1.25 0 0 0 3 2.75v5A1.25 1.25 0 0 0 4.25 9h.75" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
2648
+ variable: '<svg viewBox="0 0 16 16"><path d="M5 2.5c-1.5 1.5-2 3.5-2 5.5s.5 4 2 5.5M11 2.5c1.5 1.5 2 3.5 2 5.5s-.5 4-2 5.5M6.5 6l3 4M9.5 6l-3 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
2649
+ path: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="12.5" r="1.5" fill="currentColor"/><circle cx="12.5" cy="3.5" r="1.5" fill="currentColor"/><path d="M3.5 11V8a2 2 0 0 1 2-2h5a2 2 0 0 0 2-2" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
2112
2650
  open: '<svg viewBox="0 0 16 16"><path d="M2 4.5a1 1 0 0 1 1-1h3.2l1.3 1.5H13a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>',
2113
2651
  save: '<svg viewBox="0 0 16 16"><path d="M8 2.5v7M5 7l3 3 3-3M3 11.5v1.5h10v-1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
2114
2652
  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>',
@@ -2136,7 +2674,10 @@
2136
2674
 
2137
2675
  function getJSValueComponentProps() {
2138
2676
  return {
2139
- props: ['description', 'path', 'name', 'isDimName'],
2677
+ // keyPath is the property keys leading to this value from the logged
2678
+ // value (eg. ["people", "0", "name"]) or null if it can't be reached by
2679
+ // property keys (eg. inside of a Map).
2680
+ props: ['description', 'path', 'name', 'isDimName', 'keyPath'],
2140
2681
  data() {
2141
2682
  return {
2142
2683
  isExpanded: false,
@@ -2148,7 +2689,12 @@
2148
2689
  if (newValue && !this.hasBeenExpanded) {
2149
2690
  this.hasBeenExpanded = newValue;
2150
2691
  }
2151
- }
2692
+ },
2693
+ // When a refresh replaces this value's description, its new contents are
2694
+ // requested if it is still expanded.
2695
+ isPartialDescription(isPartial) {
2696
+ if (isPartial && this.isExpanded) this.requestDescription();
2697
+ },
2152
2698
  },
2153
2699
  computed: {
2154
2700
  isPartialDescription() {
@@ -2169,10 +2715,20 @@
2169
2715
  ];
2170
2716
  },
2171
2717
  entryGroups() {
2172
- // protoEntries only ever contains the [[Prototype]] entry.
2718
+ // protoEntries only ever contains the [[Prototype]] entry whose
2719
+ // members can be reached directly so it adds nothing to the key path.
2720
+ const {keyPath, description} = this;
2173
2721
  return [
2174
- { key: 'entries', isDim: entry => entry[2] === false },
2175
- { key: 'protoEntries', isDim: () => true },
2722
+ {
2723
+ key: 'entries',
2724
+ isDim: entry => entry[2] === false,
2725
+ getKeyPath: entry => keyPath && description.entriesArePropertyKeys ? keyPath.concat([entry[0]]) : null,
2726
+ },
2727
+ {
2728
+ key: 'protoEntries',
2729
+ isDim: () => true,
2730
+ getKeyPath: () => keyPath,
2731
+ },
2176
2732
  ];
2177
2733
  }
2178
2734
  },
@@ -2180,14 +2736,26 @@
2180
2736
  toggleExpanded() {
2181
2737
  if (!this.isExpandable) return;
2182
2738
  this.isExpanded = !this.isExpanded;
2183
- if (this.isExpanded && this.isPartialDescription) {
2184
- messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2185
- }
2186
- }
2739
+ if (this.isExpanded && this.isPartialDescription) this.requestDescription();
2740
+ },
2741
+ requestDescription() {
2742
+ messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2743
+ },
2744
+ /**
2745
+ * Shows the menu for this value (eg. Copy as JSON).
2746
+ * @param {MouseEvent} evt
2747
+ */
2748
+ openMenu(evt) {
2749
+ this.$root.openValueMenu(evt, {
2750
+ path: this.path,
2751
+ keyPath: this.keyPath,
2752
+ isPrimitive: this.description.isPrimitive,
2753
+ });
2754
+ },
2187
2755
  },
2188
2756
  template: `
2189
2757
  <div :class="classNames">
2190
- <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
2758
+ <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded" @contextmenu.prevent.stop="openMenu"><span
2191
2759
  v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
2192
2760
  v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
2193
2761
  v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
@@ -2200,6 +2768,7 @@
2200
2768
  :name="entry[0]"
2201
2769
  :is-dim-name="group.isDim(entry)"
2202
2770
  :description="entry[1]"
2771
+ :key-path="group.getKeyPath(entry)"
2203
2772
  :path="path.concat([group.key, entryIndex])">
2204
2773
  </js-value>
2205
2774
  </template>
@@ -2302,6 +2871,88 @@
2302
2871
  };
2303
2872
  }
2304
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
+
2305
2956
  /**
2306
2957
  * Gets a block of code ready to run: packages imported by name (eg.
2307
2958
  * `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
@@ -2367,6 +3018,22 @@
2367
3018
  return {code, resultRange};
2368
3019
  }
2369
3020
 
3021
+ /**
3022
+ * Turns property keys into a path like the browser's console's "Copy property
3023
+ * path" (eg. ["people", "0", "first name"] becomes `people[0]["first name"]`).
3024
+ * @param {string[]} keys
3025
+ * @returns {string}
3026
+ */
3027
+ function formatPropertyPath(keys) {
3028
+ return keys.map((key, index) =>
3029
+ /^(0|[1-9]\d*)$/.test(key)
3030
+ ? `[${key}]`
3031
+ : /^[A-Za-z_$][\w$]*$/.test(key)
3032
+ ? (index ? '.' : '') + key
3033
+ : `[${JSON.stringify(key)}]`
3034
+ ).join('');
3035
+ }
3036
+
2370
3037
  /**
2371
3038
  * Indicates if an import specifier is a package name (eg. "lodash",
2372
3039
  * "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
@@ -2525,6 +3192,7 @@
2525
3192
  `${Object(part)[1]}`,
2526
3193
  ]),
2527
3194
  ...sanitizeEntries(description),
3195
+ ...(description.entriesArePropertyKeys != null ? {entriesArePropertyKeys: !!description.entriesArePropertyKeys} : {}),
2528
3196
  };
2529
3197
  }
2530
3198
 
@@ -2615,7 +3283,47 @@
2615
3283
  level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
2616
3284
  }
2617
3285
 
2618
- if (level) Object.assign(level, sanitizeEntries(description));
3286
+ if (!level) return;
3287
+ Object.assign(level, sanitizeEntries(description));
3288
+ level.entriesArePropertyKeys = !!Object(description).entriesArePropertyKeys;
3289
+ // A refresh also sends the value's preview.
3290
+ if (Array.isArray(Object(description).parts)) {
3291
+ const {typeName, isPrimitive, parts} = sanitizeDescription(description);
3292
+ Object.assign(level, {typeName, isPrimitive, parts});
3293
+ }
3294
+ }
3295
+
3296
+ /**
3297
+ * NOTE: Called via main by the runner with a value converted to JSON (see
3298
+ * requestValueJson()).
3299
+ * @param {{requestId: string, json?: string, circularCount?: number, error?: string}} result
3300
+ */
3301
+ function onValueJson(result) {
3302
+ const {requestId, json, circularCount, error} = Object(result);
3303
+ const resolve = jsonRequests.get(`${requestId}`);
3304
+ if (!resolve) return;
3305
+ jsonRequests.delete(`${requestId}`);
3306
+ resolve({
3307
+ json: json != null ? `${json}` : null,
3308
+ circularCount: +circularCount || 0,
3309
+ error: error != null ? `${error}` : null,
3310
+ });
3311
+ }
3312
+
3313
+ /**
3314
+ * The functions waiting for values to be converted to JSON by their request
3315
+ * IDs.
3316
+ * @type {Map<string, (result: {json: string?, circularCount: number, error: string?}) => void>}
3317
+ */
3318
+ const jsonRequests = new Map();
3319
+
3320
+ /**
3321
+ * NOTE: Called via main by the runner after a value was stored as a global
3322
+ * variable.
3323
+ * @param {string} name
3324
+ */
3325
+ function onStoredAsGlobal(name) {
3326
+ mountedApp.displays.push({type: 'notice', message: `Stored as the global variable ${`${name}`}`});
2619
3327
  }
2620
3328
 
2621
3329
  /**
@@ -2685,6 +3393,8 @@
2685
3393
  // Used to find the last expression in each block of code (so that its
2686
3394
  // value can be shown) and the packages that it imports.
2687
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')] : [],
2688
3398
  ],
2689
3399
  cssUrls: [
2690
3400
  'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
@@ -2703,6 +3413,7 @@
2703
3413
  this.call('init', code, dataset, {
2704
3414
  runnerMode,
2705
3415
  blockType,
3416
+ language,
2706
3417
  showResults,
2707
3418
  importsUrl,
2708
3419
  packageInfo: PACKAGE_INFO,
@@ -2779,7 +3490,7 @@
2779
3490
  * The options of YourJSBox.create() which are the same as the data
2780
3491
  * attributes of a script tag.
2781
3492
  */
2782
- 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'];
2783
3494
 
2784
3495
  /**
2785
3496
  * The JavaScript API for creating consoles (available as window.YourJSBox).
@@ -2805,6 +3516,7 @@
2805
3516
  * Optional. The code that the console starts with.
2806
3517
  * @param {string=} options.runner
2807
3518
  * @param {string=} options.blockType
3519
+ * @param {string=} options.language
2808
3520
  * @param {(boolean|string)=} options.showResults
2809
3521
  * @param {string=} options.hidePrefix
2810
3522
  * @param {string=} options.dividerOrient