yourjs-box 1.4.0 → 1.6.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.4.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.6.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}.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.4.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.6.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
13
13
  /**
14
14
  * Viewer IFRAME's HTML code
15
15
  * @type {string}
16
16
  */
17
- const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
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.
@@ -62,14 +62,35 @@
62
62
  return new URL(baseUrl + path, document.baseURI).href;
63
63
  }
64
64
 
65
+ /**
66
+ * Where packages imported by name (eg. `import _ from 'lodash'`) are loaded
67
+ * from unless data-imports-url is given. `{specifier}` is replaced with what
68
+ * was imported (eg. "lodash@4/fp").
69
+ */
70
+ const DEFAULT_IMPORTS_URL = 'https://esm.sh/{specifier}';
71
+
72
+ /**
73
+ * Makes the part of a URL template before its first placeholder absolute
74
+ * (relative to the page) because the code runs somewhere (eg. a worker)
75
+ * that doesn't know the page's URL.
76
+ * @param {string} template
77
+ * @returns {string}
78
+ */
79
+ function resolveUrlTemplate(template) {
80
+ const index = template.indexOf('{');
81
+ const prefix = index < 0 ? template : template.slice(0, index);
82
+ return new URL(prefix || './', document.baseURI).href.replace(/\/$/, prefix.endsWith('/') || !prefix ? '/' : '')
83
+ + (index < 0 ? '' : template.slice(index));
84
+ }
85
+
65
86
  /**
66
87
  * The only functions that may be relayed to the viewer and to the runner.
67
88
  * The runner executes the user's code so anything it sends must be limited
68
89
  * to these calls.
69
90
  */
70
91
  const RELAYABLE_FUNCS = {
71
- viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
72
- runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
92
+ viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'onStoredAsGlobal', 'onValueJson', 'updateDescriptionFor'],
93
+ runner: ['clearLogs', 'getValueAsJson', 'refreshDescription', 'reset', 'runCode', 'sendDescriptionFor', 'storeAsGlobal'],
73
94
  // Things that the viewer (but never the runner) can ask this script to do.
74
95
  host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
75
96
  };
@@ -285,10 +306,12 @@
285
306
  * @param {boolean=} isInPromise
286
307
  */
287
308
  function reportUncaught(error, isInPromise) {
288
- // Points out how to use top-level await if it was used in a classic block.
289
- const hint = runningBlockType === 'classic' && error?.name === 'SyntaxError' && /\bawait\b/.test(error.message)
290
- ? '\n(To use top-level await, add data-block-type="module" to the script tag.)'
291
- : '';
309
+ // Points out how to use top-level await or import statements if they were
310
+ // used in a classic block.
311
+ const feature = runningBlockType === 'classic' && error?.name === 'SyntaxError'
312
+ ? /\bawait\b/.test(error.message) ? 'top-level await' : /\bimport\b/.test(error.message) ? 'import statements' : null
313
+ : null;
314
+ const hint = feature ? `\n(To use ${feature}, add data-block-type="module" to the script tag.)` : '';
292
315
  send({
293
316
  target: 'viewer',
294
317
  func: 'appendError',
@@ -608,9 +631,9 @@
608
631
  const $entries = [];
609
632
  const protoEntries = [];
610
633
  const $protoEntries = [];
634
+ let wasIterated = false;
611
635
 
612
636
  if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
613
- let wasIterated = false;
614
637
  try {
615
638
  // Map
616
639
  if (typeName === 'Map') {
@@ -658,7 +681,9 @@
658
681
  }
659
682
  }
660
683
 
661
- return {entries, protoEntries, $entries, $protoEntries};
684
+ // Map and Set entries (and other iterated values) aren't properties so
685
+ // they have no property path.
686
+ return {entries, protoEntries, entriesArePropertyKeys: !wasIterated, $entries, $protoEntries};
662
687
  }
663
688
 
664
689
  // ['123', 0, 'entries', 0]
@@ -769,16 +794,25 @@
769
794
  * entry indices.
770
795
  */
771
796
  function sendDescriptionFor(path) {
772
- // Get the description of the desired value.
773
- let level = logArgsById;
774
- let pathPartIndex = 0;
775
- for (let pathPart of path) {
776
- if (pathPartIndex && pathPartIndex % 2 === 0) {
777
- pathPart = '$' + pathPart;
778
- }
779
- level = level?.[pathPart];
780
- pathPartIndex++;
781
- }
797
+ sendDescription(path, false);
798
+ }
799
+
800
+ /**
801
+ * Describes the value again (eg. after the code changed it) including its
802
+ * preview.
803
+ * @param {(string|number)[]} path
804
+ */
805
+ function refreshDescription(path) {
806
+ sendDescription(path, true);
807
+ }
808
+
809
+ /**
810
+ * @param {(string|number)[]} path
811
+ * @param {boolean} isRefresh
812
+ * If `true` the value's preview is included too.
813
+ */
814
+ function sendDescription(path, isRefresh) {
815
+ const level = getLevel(path);
782
816
  if (!level) return;
783
817
  const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
784
818
 
@@ -786,11 +820,95 @@
786
820
  Object.assign(level, description);
787
821
 
788
822
  // Send the description without values back to the viewer.
823
+ const summary = isRefresh
824
+ ? summarize(level.value, path.length > 2 ? 1 : 0, path[path.length - 2] === 'protoEntries')
825
+ : {};
789
826
  send({
790
827
  target: 'viewer',
791
828
  func: 'updateDescriptionFor',
792
- args: [path, without(['$entries', '$protoEntries'], description)],
829
+ args: [path, {...without(['$entries', '$protoEntries'], description), ...summary}],
830
+ });
831
+ }
832
+
833
+ /**
834
+ * Finds what is stored for a logged value (or a value within it).
835
+ * @param {(string|number)[]} path
836
+ * The log ID, the argument index and then pairs of entry group keys and
837
+ * entry indices.
838
+ * @returns {{value: *, receiver?: *}|undefined}
839
+ */
840
+ function getLevel(path) {
841
+ let level = logArgsById;
842
+ path.forEach((pathPart, index) => {
843
+ level = level?.[index && index % 2 === 0 ? '$' + pathPart : pathPart];
793
844
  });
845
+ return level;
846
+ }
847
+
848
+ /**
849
+ * Sends a value as JSON to the viewer (eg. to copy or save it).
850
+ * @param {(string|number)[]} path
851
+ * @param {string} requestId
852
+ */
853
+ function getValueAsJson(path, requestId) {
854
+ const level = getLevel(path);
855
+ let result;
856
+ try {
857
+ result = level ? toJson(level.value) : {error: 'The value is no longer available.'};
858
+ }
859
+ catch (e) {
860
+ result = {error: `The value couldn't be converted to JSON (${e?.message ?? e}).`};
861
+ }
862
+ send({target: 'viewer', func: 'onValueJson', args: [{requestId, ...result}]});
863
+ }
864
+
865
+ /**
866
+ * Converts a value to JSON like JSON.stringify() does except that Maps and
867
+ * Sets become arrays, BigInts become strings and circular references (an
868
+ * object inside of itself) are left out instead of causing an error. An
869
+ * object that appears more than once without being circular is kept.
870
+ * @param {*} value
871
+ * @returns {{json?: string, circularCount: number, error?: string}}
872
+ */
873
+ function toJson(value) {
874
+ let circularCount = 0;
875
+ // The objects currently being converted along with what they were
876
+ // converted from (eg. a Map is converted to an array).
877
+ const stack = [];
878
+ const json = JSON.stringify(value, function(key, val) {
879
+ // `this` is the object containing `key` so anything after it on the
880
+ // stack has already been converted.
881
+ while (stack.length && stack[stack.length - 1].converted !== this) stack.pop();
882
+
883
+ if ('bigint' === typeof val) return `${val}`;
884
+ if (val !== null && 'object' === typeof val) {
885
+ const original = val;
886
+ if (stack.some(item => item.original === original)) {
887
+ circularCount++;
888
+ return undefined;
889
+ }
890
+ if (val instanceof Map || val instanceof Set) val = [...val];
891
+ stack.push({original, converted: val});
892
+ }
893
+ return val;
894
+ }, 2);
895
+ return json === undefined
896
+ ? {error: 'This value can\'t be represented as JSON.', circularCount}
897
+ : {json, circularCount};
898
+ }
899
+
900
+ /**
901
+ * Stores a value as a global variable (temp1, temp2, etc.) like the
902
+ * browser's console does so that code can use it.
903
+ * @param {(string|number)[]} path
904
+ */
905
+ function storeAsGlobal(path) {
906
+ const level = getLevel(path);
907
+ if (!level) return;
908
+ let index = 1;
909
+ while (`temp${index}` in globalThis) index++;
910
+ globalThis[`temp${index}`] = level.value;
911
+ send({target: 'viewer', func: 'onStoredAsGlobal', args: [`temp${index}`]});
794
912
  }
795
913
 
796
914
  /**
@@ -801,7 +919,7 @@
801
919
  groupIds = [];
802
920
  }
803
921
 
804
- return {clearLogs, destroy, runCode, sendDescriptionFor};
922
+ return {clearLogs, destroy, getValueAsJson, refreshDescription, runCode, sendDescriptionFor, storeAsGlobal};
805
923
  }
806
924
 
807
925
 
@@ -928,6 +1046,10 @@
928
1046
  const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
929
1047
  const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
930
1048
  const showResults = dataset.showResults !== 'false';
1049
+ // An empty data-imports-url turns off loading packages by name.
1050
+ const importsUrl = dataset.importsUrl == null
1051
+ ? DEFAULT_IMPORTS_URL
1052
+ : dataset.importsUrl && resolveUrlTemplate(dataset.importsUrl);
931
1053
  const libraryUrl = getLibraryFileUrl.bind(null, dataset.librariesUrl || DEFAULT_LIBRARIES_URL);
932
1054
 
933
1055
  // The viewer in the page and, when the console is popped out, the viewer
@@ -1117,8 +1239,8 @@
1117
1239
 
1118
1240
  const hidePrefix = dataset.hidePrefix ?? '';
1119
1241
  const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
1120
- const originalCode = unindentMin(jsCode);
1121
- const {visibleCode, hiddenGroups} = extractHiddenGroups(originalCode, hidePrefix);
1242
+ const startingCode = unindentMin(jsCode);
1243
+ const {visibleCode, hiddenGroups} = extractHiddenGroups(startingCode, hidePrefix);
1122
1244
  const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
1123
1245
  const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
1124
1246
 
@@ -1130,6 +1252,12 @@
1130
1252
  hidePrefix,
1131
1253
  hiddenGroups: copyHiddenGroups(),
1132
1254
  runnerMode: meta.runnerMode,
1255
+ importsUrl: meta.importsUrl,
1256
+ // The code the console starts with (and goes back to when reset)
1257
+ // which changes when a file is opened.
1258
+ originalCode: startingCode,
1259
+ // The name of the file that was last opened or saved.
1260
+ fileName: null,
1133
1261
  blockType: meta.blockType,
1134
1262
  showResults: meta.showResults,
1135
1263
  packageInfo: meta.packageInfo,
@@ -1166,6 +1294,18 @@
1166
1294
  dividerSize: '8px',
1167
1295
  tempDividerPct: null,
1168
1296
  isMenuOpen: false,
1297
+ isHistoryOpen: false,
1298
+ // The code that was run (newest last) which can be brought back into
1299
+ // the editor like the browser's console (eg. with the up arrow).
1300
+ commandHistory: [],
1301
+ // While going through the history: the index of the code shown and
1302
+ // the unrun code that was in the editor (shown again at the end).
1303
+ historyIndex: null,
1304
+ historyDraft: null,
1305
+ /** @type {{x: number, y: number, path: any[], propertyPath: string?, isPrimitive: boolean}?} */
1306
+ valueMenu: null,
1307
+ /** @type {{message: string}?} */
1308
+ toast: null,
1169
1309
  textScale: loadTextScale(),
1170
1310
  isFullscreen: false,
1171
1311
  // Set when full screen isn't allowed so the console fills the page.
@@ -1233,12 +1373,23 @@
1233
1373
  /**
1234
1374
  * The code that the copied console will start with.
1235
1375
  */
1376
+ /**
1377
+ * The original code split into the code for the editor and the
1378
+ * hidden groups.
1379
+ */
1380
+ originalGroups() {
1381
+ return extractHiddenGroups(this.originalCode, this.hidePrefix);
1382
+ },
1236
1383
  exportJsCode() {
1237
- if (this.exportCode === 'original') return originalCode;
1384
+ if (this.exportCode === 'original') return this.originalCode;
1238
1385
  if (this.exportCode === 'blank') return '';
1239
-
1240
- // The groups that already ran, then the groups in the editor with
1241
- // any hidden groups that haven't run yet in their original places.
1386
+ return this.currentCode;
1387
+ },
1388
+ /**
1389
+ * The groups that already ran, then the groups in the editor with any
1390
+ * hidden groups that haven't run yet in their original places.
1391
+ */
1392
+ currentCode() {
1242
1393
  const blocks = this.runHistory.slice();
1243
1394
  const pendingHiddenGroups = this.hiddenGroups.slice();
1244
1395
  let visibleCount = this.runCount;
@@ -1277,11 +1428,23 @@
1277
1428
  bottomButtons() {
1278
1429
  const isFullscreen = this.isFullscreen || this.isMaximized;
1279
1430
  return [
1431
+ {
1432
+ iconName: 'more',
1433
+ title: 'More',
1434
+ className: 'more-button',
1435
+ callback() { this.toggleMenu(); },
1436
+ },
1280
1437
  {
1281
1438
  iconName: 'clear',
1282
1439
  title: 'Clear console',
1283
1440
  callback() { this.clearConsole(); },
1284
1441
  },
1442
+ {
1443
+ iconName: 'history',
1444
+ title: `History (${isMac ? '\u2325' : 'Alt+'}H)`,
1445
+ className: 'history-button',
1446
+ callback() { this.toggleHistory(); },
1447
+ },
1285
1448
  {
1286
1449
  iconName: 'horizontalView',
1287
1450
  title: 'Show the editor below the console',
@@ -1299,12 +1462,6 @@
1299
1462
  title: isFullscreen ? 'Exit full screen' : 'Full screen',
1300
1463
  callback() { this.toggleFullscreen(); },
1301
1464
  },
1302
- {
1303
- iconName: 'more',
1304
- title: 'More',
1305
- className: 'more-button',
1306
- callback() { this.toggleMenu(); },
1307
- },
1308
1465
  {
1309
1466
  iconName: this.runningCount ? 'spinner' : 'play',
1310
1467
  label: 'Run',
@@ -1315,6 +1472,20 @@
1315
1472
  },
1316
1473
  ].filter(btn => !btn.showIf || btn.showIf.call(this));
1317
1474
  },
1475
+ /**
1476
+ * The history's entries (newest first) for the history list.
1477
+ */
1478
+ historyEntries() {
1479
+ return this.commandHistory.map((code, index) => {
1480
+ const [group] = parseJSCodeGroups(code);
1481
+ const lines = (group?.lines ?? code).split('\n').filter(line => line.trim());
1482
+ return {
1483
+ index,
1484
+ label: group?.headerLines || lines[0] || '',
1485
+ detail: `${lines.length} line${lines.length === 1 ? '' : 's'}`,
1486
+ };
1487
+ }).reverse();
1488
+ },
1318
1489
  canShrinkText() {
1319
1490
  return this.textScale > TEXT_SCALES[0];
1320
1491
  },
@@ -1355,9 +1526,110 @@
1355
1526
  this.runGroup(jsCodeGroup0);
1356
1527
 
1357
1528
  // Remove the code that was run from the editor.
1358
- this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1529
+ const remainingCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1359
1530
 
1360
- this.runCount++;
1531
+ if (this.historyIndex !== null) {
1532
+ // Code from the history was run so the unrun code comes back. It
1533
+ // doesn't count as moving through the code so hidden blocks still
1534
+ // run in their places.
1535
+ this.jsCode = [remainingCode, this.historyDraft].filter(code => code.trim()).join('\n');
1536
+ this.historyIndex = this.historyDraft = null;
1537
+ }
1538
+ else {
1539
+ this.jsCode = remainingCode;
1540
+ this.runCount++;
1541
+ }
1542
+ },
1543
+ /**
1544
+ * Shows older (-1) or newer (1) code from the history in the editor.
1545
+ * Going newer than the newest shows the unrun code again.
1546
+ * @param {-1|1} direction
1547
+ * @returns {boolean}
1548
+ * `true` if the history was used (so the key shouldn't do anything
1549
+ * else).
1550
+ */
1551
+ navigateHistory(direction) {
1552
+ const history = this.commandHistory;
1553
+ if (direction < 0) {
1554
+ if (!history.length) return false;
1555
+ if (this.historyIndex === null) {
1556
+ this.historyDraft = this.jsCode;
1557
+ this.historyIndex = history.length;
1558
+ }
1559
+ if (this.historyIndex > 0) this.showHistoryCode(this.historyIndex - 1, 'start');
1560
+ return true;
1561
+ }
1562
+ if (this.historyIndex === null) return false;
1563
+ if (this.historyIndex < history.length - 1) {
1564
+ this.showHistoryCode(this.historyIndex + 1, 'end');
1565
+ }
1566
+ else {
1567
+ const draft = this.historyDraft;
1568
+ this.historyIndex = this.historyDraft = null;
1569
+ this.setEditorCode(draft, 'end');
1570
+ }
1571
+ return true;
1572
+ },
1573
+ /**
1574
+ * Shows code from the history in the editor (saving the unrun code if
1575
+ * this is the first time).
1576
+ * @param {number} index
1577
+ * @param {"start"|"end"} cursorAt
1578
+ */
1579
+ showHistoryCode(index, cursorAt) {
1580
+ if (this.historyIndex === null) this.historyDraft = this.jsCode;
1581
+ this.historyIndex = index;
1582
+ this.setEditorCode(this.commandHistory[index], cursorAt);
1583
+ },
1584
+ /**
1585
+ * @param {string} code
1586
+ * @param {"start"|"end"} cursorAt
1587
+ * Where the cursor goes so that pressing the same arrow key again
1588
+ * keeps going through the history.
1589
+ */
1590
+ setEditorCode(code, cursorAt) {
1591
+ this.jsCode = code;
1592
+ this.$nextTick(() => {
1593
+ const editor = this.$refs.editor?.editor;
1594
+ if (!editor) return;
1595
+ if (cursorAt === 'end') editor.navigateFileEnd();
1596
+ else editor.navigateFileStart();
1597
+ });
1598
+ },
1599
+ /**
1600
+ * Called by the editor when the up or down arrow is pressed with the
1601
+ * cursor at the very start or end (and nothing selected).
1602
+ * @param {{direction: -1|1, handled: boolean}} event
1603
+ */
1604
+ onEditorHistoryKey(event) {
1605
+ event.handled = this.navigateHistory(event.direction);
1606
+ },
1607
+ toggleHistory() {
1608
+ if (this.isHistoryOpen) {
1609
+ this.isHistoryOpen = false;
1610
+ }
1611
+ else {
1612
+ this.closeMenu();
1613
+ this.closeValueMenu();
1614
+ this.isHistoryOpen = true;
1615
+ this.$nextTick(() => this.$refs.historyPanel?.querySelector('.menu-item')?.focus());
1616
+ }
1617
+ },
1618
+ /**
1619
+ * Puts code from the history list into the editor.
1620
+ * @param {number} index
1621
+ */
1622
+ recallHistory(index) {
1623
+ this.isHistoryOpen = false;
1624
+ this.showHistoryCode(index, 'start');
1625
+ this.$nextTick(() => this.$refs.editor?.editor.focus());
1626
+ },
1627
+ /**
1628
+ * Forgets that the history was being gone through (eg. when the code in
1629
+ * the editor is replaced).
1630
+ */
1631
+ stopNavigatingHistory() {
1632
+ this.historyIndex = this.historyDraft = null;
1361
1633
  },
1362
1634
  /**
1363
1635
  * Runs the next hidden group if all of the visible code that came
@@ -1385,17 +1657,23 @@
1385
1657
  });
1386
1658
  this.runningCount++;
1387
1659
  this.runHistory.push(group.allLines);
1660
+ // Hidden code wasn't typed or seen so it isn't in the history.
1661
+ const historyCode = group.allLines.trim();
1662
+ if (!isHidden && historyCode && this.commandHistory[this.commandHistory.length - 1] !== historyCode) {
1663
+ this.commandHistory.push(historyCode);
1664
+ if (this.commandHistory.length > 200) this.commandHistory.shift();
1665
+ }
1666
+ const {code, resultRange} = prepareCode(group.lines, {
1667
+ blockType: this.blockType,
1668
+ importsUrl: this.importsUrl,
1669
+ // Like the browser's console, show the value of the last
1670
+ // expression (except for hidden code).
1671
+ findResult: this.showResults && !isHidden,
1672
+ });
1388
1673
  messageParent({
1389
1674
  target: 'runner',
1390
1675
  func: 'runCode',
1391
- args: [group.lines, {
1392
- blockType: this.blockType,
1393
- // Like the browser's console, show the value of the last
1394
- // expression (except for hidden code).
1395
- resultRange: this.showResults && !isHidden
1396
- ? findLastExpression(group.lines, this.blockType)
1397
- : null,
1398
- }],
1676
+ args: [code, {blockType: this.blockType, resultRange}],
1399
1677
  });
1400
1678
  },
1401
1679
  clearConsole() {
@@ -1412,16 +1690,88 @@
1412
1690
  confirmText: 'Reset',
1413
1691
  });
1414
1692
  if (!isConfirmed) return;
1415
-
1693
+ this.restart();
1694
+ },
1695
+ /**
1696
+ * Starts over with the original code (clearing the output and, in
1697
+ * worker mode, starting a new worker).
1698
+ */
1699
+ restart() {
1700
+ const {visibleCode, hiddenGroups} = this.originalGroups;
1416
1701
  this.displays = [];
1417
1702
  this.jsCode = visibleCode;
1418
- this.hiddenGroups = copyHiddenGroups();
1703
+ this.hiddenGroups = hiddenGroups.map(group => ({...group}));
1419
1704
  this.runCount = 0;
1420
1705
  this.runningCount = 0;
1421
1706
  this.runHistory = [];
1707
+ this.stopNavigatingHistory();
1422
1708
  messageParent({target: 'runner', func: 'reset', args: []});
1423
1709
  this.runHiddenGroups();
1424
1710
  },
1711
+ /**
1712
+ * Shows the file picker for opening a JavaScript file.
1713
+ */
1714
+ openFile() {
1715
+ this.closeMenu();
1716
+ const {fileInput} = this.$refs;
1717
+ fileInput.value = '';
1718
+ fileInput.click();
1719
+ },
1720
+ /**
1721
+ * Makes the chosen file's code the code that the console starts with.
1722
+ * @param {Event} evt
1723
+ */
1724
+ async onFileChosen(evt) {
1725
+ const file = evt.target.files?.[0];
1726
+ if (!file) return;
1727
+ const code = unindentMin(await file.text());
1728
+ if (this.displays.length || this.jsCode.trim()) {
1729
+ const isConfirmed = await this.confirm({
1730
+ title: `Open ${file.name}?`,
1731
+ message: 'The output will be cleared and the code in the editor will be replaced with the code in the file.'
1732
+ + (this.runnerMode === 'window'
1733
+ ? ' Anything the code already defined on the page will stay defined.'
1734
+ : ''),
1735
+ confirmText: 'Open',
1736
+ });
1737
+ if (!isConfirmed) return;
1738
+ }
1739
+ this.fileName = file.name;
1740
+ this.originalCode = code;
1741
+ this.restart();
1742
+ },
1743
+ /**
1744
+ * Saves the current code (what already ran and what is in the editor)
1745
+ * as a JavaScript file which can be opened later.
1746
+ */
1747
+ async saveFile() {
1748
+ this.closeMenu();
1749
+ const code = this.currentCode + '\n';
1750
+ const suggestedName = this.fileName || 'js-box.js';
1751
+
1752
+ // Shows a "Save as" dialog where it is supported.
1753
+ if ('function' === typeof window.showSaveFilePicker) {
1754
+ try {
1755
+ const handle = await showSaveFilePicker({
1756
+ suggestedName,
1757
+ types: [{description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}}],
1758
+ });
1759
+ const writable = await handle.createWritable();
1760
+ await writable.write(code);
1761
+ await writable.close();
1762
+ this.fileName = handle.name;
1763
+ return;
1764
+ }
1765
+ catch (e) {
1766
+ // The user cancelled. For other errors the file is downloaded.
1767
+ if (e?.name === 'AbortError') return;
1768
+ }
1769
+ }
1770
+
1771
+ const url = URL.createObjectURL(new Blob([code], {type: 'text/javascript'}));
1772
+ Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
1773
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1774
+ },
1425
1775
  /**
1426
1776
  * Copies code that was already run into the editor so that it can be run
1427
1777
  * again (as is or modified), first asking before replacing any code that
@@ -1437,6 +1787,7 @@
1437
1787
  });
1438
1788
  if (!isConfirmed) return;
1439
1789
  }
1790
+ this.stopNavigatingHistory();
1440
1791
  this.jsCode = code;
1441
1792
  },
1442
1793
  /**
@@ -1520,6 +1871,11 @@
1520
1871
  return JSON.parse(JSON.stringify({
1521
1872
  displays: this.displays,
1522
1873
  jsCode: this.jsCode,
1874
+ originalCode: this.originalCode,
1875
+ fileName: this.fileName,
1876
+ commandHistory: this.commandHistory,
1877
+ historyIndex: this.historyIndex,
1878
+ historyDraft: this.historyDraft,
1523
1879
  runHistory: this.runHistory,
1524
1880
  hiddenGroups: this.hiddenGroups,
1525
1881
  runCount: this.runCount,
@@ -1541,6 +1897,8 @@
1541
1897
  this.closeMenu();
1542
1898
  }
1543
1899
  else {
1900
+ this.isHistoryOpen = false;
1901
+ this.closeValueMenu();
1544
1902
  this.isMenuOpen = true;
1545
1903
  this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
1546
1904
  }
@@ -1553,14 +1911,21 @@
1553
1911
  * Lets the arrow keys move between the menu's items.
1554
1912
  * @param {KeyboardEvent} evt
1555
1913
  */
1914
+ /**
1915
+ * Lets Escape close a menu (or the history list) and the arrow keys move
1916
+ * between its items.
1917
+ * @param {KeyboardEvent} evt
1918
+ */
1556
1919
  onMenuKeyDown(evt) {
1557
1920
  if (evt.key === 'Escape') {
1558
1921
  evt.stopPropagation();
1559
- this.closeMenu(true);
1922
+ this.closeMenu(this.isMenuOpen);
1923
+ this.closeValueMenu();
1924
+ this.isHistoryOpen = false;
1560
1925
  }
1561
1926
  else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
1562
1927
  evt.preventDefault();
1563
- const items = [...this.$refs.moreMenu.querySelectorAll('.menu-item:not(:disabled)')];
1928
+ const items = [...evt.currentTarget.querySelectorAll('.menu-item:not(:disabled)')];
1564
1929
  const index = items.indexOf(document.activeElement);
1565
1930
  const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
1566
1931
  next?.focus();
@@ -1640,15 +2005,189 @@
1640
2005
  if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
1641
2006
  this.closeMenu();
1642
2007
  }
2008
+ if (this.valueMenu && !evt.target.closest('.value-menu')) {
2009
+ this.closeValueMenu();
2010
+ }
2011
+ if (this.isHistoryOpen && !evt.target.closest('.history-panel, .history-button')) {
2012
+ this.isHistoryOpen = false;
2013
+ }
1643
2014
  },
1644
2015
  /**
1645
2016
  * @param {KeyboardEvent} evt
1646
2017
  */
1647
2018
  onWindowKeyDown(evt) {
2019
+ if (evt.altKey && !evt.ctrlKey && !evt.metaKey && evt.code === 'KeyH') {
2020
+ evt.preventDefault();
2021
+ evt.stopPropagation();
2022
+ this.toggleHistory();
2023
+ return;
2024
+ }
2025
+ if (evt.key === 'Escape' && this.valueMenu) {
2026
+ this.closeValueMenu();
2027
+ return;
2028
+ }
1648
2029
  if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
1649
2030
  this.setMaximized(false);
1650
2031
  }
1651
2032
  },
2033
+ /**
2034
+ * Shows the menu for a value that was right-clicked.
2035
+ * @param {MouseEvent} evt
2036
+ * @param {{path: any[], keyPath: string[]?, isPrimitive: boolean}} value
2037
+ */
2038
+ openValueMenu(evt, {path, keyPath, isPrimitive}) {
2039
+ this.closeMenu();
2040
+ this.valueMenu = {
2041
+ x: evt.clientX,
2042
+ y: evt.clientY,
2043
+ path: path.slice(),
2044
+ // Only properties and items (not logged values themselves) have
2045
+ // a property path.
2046
+ propertyPath: keyPath?.length ? formatPropertyPath(keyPath) : null,
2047
+ fileName: keyPath?.length && /^[\w$-]+$/.test(keyPath[keyPath.length - 1]) ? keyPath[keyPath.length - 1] : 'value',
2048
+ isPrimitive,
2049
+ };
2050
+ // Keeps the menu inside of the window.
2051
+ this.$nextTick(() => {
2052
+ const menu = this.$refs.valueMenu;
2053
+ if (!menu || !this.valueMenu) return;
2054
+ const {width, height} = menu.getBoundingClientRect();
2055
+ this.valueMenu.x = Math.max(4, Math.min(this.valueMenu.x, innerWidth - width - 4));
2056
+ this.valueMenu.y = Math.max(4, Math.min(this.valueMenu.y, innerHeight - height - 4));
2057
+ menu.querySelector('.menu-item')?.focus();
2058
+ });
2059
+ },
2060
+ closeValueMenu() {
2061
+ this.valueMenu = null;
2062
+ },
2063
+ /**
2064
+ * Asks the runner to convert the value to JSON.
2065
+ * @param {any[]} path
2066
+ * @returns {Promise<{json: string?, circularCount: number, error: string?}>}
2067
+ */
2068
+ requestValueJson(path) {
2069
+ const requestId = `${Date.now()}-${Math.random()}`;
2070
+ return new Promise(resolve => {
2071
+ jsonRequests.set(requestId, resolve);
2072
+ messageParent({target: 'runner', func: 'getValueAsJson', args: [path, requestId]});
2073
+ });
2074
+ },
2075
+ /**
2076
+ * Shows a short message (eg. "Copied") for a few seconds.
2077
+ * @param {string} message
2078
+ */
2079
+ showToast(message) {
2080
+ clearTimeout(this.toastTimeout);
2081
+ this.toast = {message};
2082
+ this.toastTimeout = setTimeout(() => this.toast = null, 3500);
2083
+ },
2084
+ /**
2085
+ * @param {{json: string?, circularCount: number, error: string?}} result
2086
+ * @param {string} action
2087
+ * What was done (eg. "Copied as JSON").
2088
+ */
2089
+ showJsonToast({circularCount, error}, action) {
2090
+ this.showToast(
2091
+ error || action + (circularCount
2092
+ ? ` (${circularCount} circular reference${circularCount === 1 ? ' was' : 's were'} left out)`
2093
+ : '')
2094
+ );
2095
+ },
2096
+ /**
2097
+ * Copies the value as JSON. The copy is started right away (while
2098
+ * the browser still allows it) with the JSON filled in when it is
2099
+ * ready.
2100
+ */
2101
+ async copyValueAsJson() {
2102
+ // A plain copy because the menu's (reactive) path can't be sent.
2103
+ const path = [...this.valueMenu.path];
2104
+ this.closeValueMenu();
2105
+ const resultPromise = this.requestValueJson(path);
2106
+ try {
2107
+ if (window.ClipboardItem && navigator.clipboard?.write) {
2108
+ await navigator.clipboard.write([new ClipboardItem({
2109
+ 'text/plain': resultPromise.then(({json, error}) => {
2110
+ if (error) throw new Error(error);
2111
+ return new Blob([json], {type: 'text/plain'});
2112
+ }),
2113
+ })]);
2114
+ }
2115
+ else {
2116
+ const {json, error} = await resultPromise;
2117
+ if (error) throw new Error(error);
2118
+ await navigator.clipboard.writeText(json);
2119
+ }
2120
+ this.showJsonToast(await resultPromise, 'Copied as JSON');
2121
+ }
2122
+ catch (e) {
2123
+ const result = await resultPromise;
2124
+ this.showToast(result.error || 'The value couldn\'t be copied. Your browser may not allow copying here.');
2125
+ }
2126
+ },
2127
+ /**
2128
+ * Saves the value as a JSON file. The "Save as" dialog is shown right
2129
+ * away (while the browser still allows it).
2130
+ */
2131
+ async saveValueAsJson() {
2132
+ const {fileName} = this.valueMenu;
2133
+ const path = [...this.valueMenu.path];
2134
+ this.closeValueMenu();
2135
+ const resultPromise = this.requestValueJson(path);
2136
+ const suggestedName = `${fileName}.json`;
2137
+ let handle;
2138
+ if ('function' === typeof window.showSaveFilePicker) {
2139
+ try {
2140
+ handle = await showSaveFilePicker({
2141
+ suggestedName,
2142
+ types: [{description: 'JSON', accept: {'application/json': ['.json']}}],
2143
+ });
2144
+ }
2145
+ catch (e) {
2146
+ if (e?.name === 'AbortError') return;
2147
+ }
2148
+ }
2149
+ const result = await resultPromise;
2150
+ if (result.error) {
2151
+ this.showToast(result.error);
2152
+ return;
2153
+ }
2154
+ const text = result.json + '\n';
2155
+ if (handle) {
2156
+ const writable = await handle.createWritable();
2157
+ await writable.write(text);
2158
+ await writable.close();
2159
+ }
2160
+ else {
2161
+ const url = URL.createObjectURL(new Blob([text], {type: 'application/json'}));
2162
+ Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
2163
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
2164
+ }
2165
+ this.showJsonToast(result, 'Saved as JSON');
2166
+ },
2167
+ storeValueAsGlobal() {
2168
+ const path = [...this.valueMenu.path];
2169
+ this.closeValueMenu();
2170
+ messageParent({target: 'runner', func: 'storeAsGlobal', args: [path]});
2171
+ },
2172
+ async copyPropertyPath() {
2173
+ const {propertyPath} = this.valueMenu;
2174
+ this.closeValueMenu();
2175
+ try {
2176
+ await navigator.clipboard.writeText(propertyPath);
2177
+ this.showToast(`Copied ${propertyPath}`);
2178
+ }
2179
+ catch (e) {
2180
+ this.showToast('The property path couldn\'t be copied. Your browser may not allow copying here.');
2181
+ }
2182
+ },
2183
+ /**
2184
+ * Shows the value as it is now (eg. after the code changed it).
2185
+ */
2186
+ refreshValue() {
2187
+ const path = [...this.valueMenu.path];
2188
+ this.closeValueMenu();
2189
+ messageParent({target: 'runner', func: 'refreshDescription', args: [path]});
2190
+ },
1652
2191
  closeDialog(isConfirmed) {
1653
2192
  const {dialog} = this;
1654
2193
  this.dialog = null;
@@ -1669,6 +2208,7 @@
1669
2208
  }
1670
2209
  },
1671
2210
  onDisplaysScroll() {
2211
+ this.closeValueMenu();
1672
2212
  const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
1673
2213
  this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
1674
2214
  },
@@ -1742,7 +2282,8 @@
1742
2282
  mounted() {
1743
2283
  addEventListener('mousemove', this.onWindowMouseMove);
1744
2284
  addEventListener('mousedown', this.onWindowMouseDown);
1745
- addEventListener('keydown', this.onWindowKeyDown);
2285
+ // Capturing lets shortcuts (eg. Alt+H) work before the editor sees them.
2286
+ addEventListener('keydown', this.onWindowKeyDown, true);
1746
2287
  addEventListener('resize', this.onWindowResize);
1747
2288
  addEventListener('mouseup', this.onWindowMouseUp);
1748
2289
  addEventListener('error', this.onWindowError);
@@ -1888,6 +2429,28 @@
1888
2429
  });
1889
2430
 
1890
2431
  // Attempt to capture key combos.
2432
+ // Like the browser's console, the up arrow at the very start (or the
2433
+ // down arrow at the very end) with nothing selected goes through the
2434
+ // history. This listens while capturing so it runs before Ace moves the
2435
+ // cursor.
2436
+ this.$refs.editor.addEventListener('keydown', evt => {
2437
+ const isUp = evt.key === 'ArrowUp';
2438
+ if (!isUp && evt.key !== 'ArrowDown') return;
2439
+ if (evt.shiftKey || evt.altKey || evt.ctrlKey || evt.metaKey || !editor.selection.isEmpty()) return;
2440
+ const {row, column} = editor.getCursorPosition();
2441
+ const lastRow = editor.session.getLength() - 1;
2442
+ const isAtEdge = isUp
2443
+ ? row === 0 && column === 0
2444
+ : row === lastRow && column === editor.session.getLine(lastRow).length;
2445
+ if (!isAtEdge) return;
2446
+ const event = {direction: isUp ? -1 : 1, handled: false};
2447
+ this.$emit('historyKey', event);
2448
+ if (event.handled) {
2449
+ evt.preventDefault();
2450
+ evt.stopPropagation();
2451
+ }
2452
+ }, true);
2453
+
1891
2454
  editor.textInput.getElement().addEventListener('keydown', (evt) => {
1892
2455
  const isComboKeyCode = evt.keyCode === 16 // SHIFT
1893
2456
  || evt.keyCode === 17 // CTRL
@@ -1992,6 +2555,12 @@
1992
2555
  // https://icon-sets.iconify.design/mdi/error-outline/
1993
2556
  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>',
1994
2557
  // Icons similar to those in the browser's console.
2558
+ 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>',
2559
+ 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>',
2560
+ 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>',
2561
+ 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>',
2562
+ 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>',
2563
+ 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>',
1995
2564
  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>',
1996
2565
  exitFullscreen: '<svg viewBox="0 0 16 16"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1997
2566
  more: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="1.4" fill="currentColor"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/><circle cx="12.5" cy="8" r="1.4" fill="currentColor"/></svg>',
@@ -2017,7 +2586,10 @@
2017
2586
 
2018
2587
  function getJSValueComponentProps() {
2019
2588
  return {
2020
- props: ['description', 'path', 'name', 'isDimName'],
2589
+ // keyPath is the property keys leading to this value from the logged
2590
+ // value (eg. ["people", "0", "name"]) or null if it can't be reached by
2591
+ // property keys (eg. inside of a Map).
2592
+ props: ['description', 'path', 'name', 'isDimName', 'keyPath'],
2021
2593
  data() {
2022
2594
  return {
2023
2595
  isExpanded: false,
@@ -2029,7 +2601,12 @@
2029
2601
  if (newValue && !this.hasBeenExpanded) {
2030
2602
  this.hasBeenExpanded = newValue;
2031
2603
  }
2032
- }
2604
+ },
2605
+ // When a refresh replaces this value's description, its new contents are
2606
+ // requested if it is still expanded.
2607
+ isPartialDescription(isPartial) {
2608
+ if (isPartial && this.isExpanded) this.requestDescription();
2609
+ },
2033
2610
  },
2034
2611
  computed: {
2035
2612
  isPartialDescription() {
@@ -2050,10 +2627,20 @@
2050
2627
  ];
2051
2628
  },
2052
2629
  entryGroups() {
2053
- // protoEntries only ever contains the [[Prototype]] entry.
2630
+ // protoEntries only ever contains the [[Prototype]] entry whose
2631
+ // members can be reached directly so it adds nothing to the key path.
2632
+ const {keyPath, description} = this;
2054
2633
  return [
2055
- { key: 'entries', isDim: entry => entry[2] === false },
2056
- { key: 'protoEntries', isDim: () => true },
2634
+ {
2635
+ key: 'entries',
2636
+ isDim: entry => entry[2] === false,
2637
+ getKeyPath: entry => keyPath && description.entriesArePropertyKeys ? keyPath.concat([entry[0]]) : null,
2638
+ },
2639
+ {
2640
+ key: 'protoEntries',
2641
+ isDim: () => true,
2642
+ getKeyPath: () => keyPath,
2643
+ },
2057
2644
  ];
2058
2645
  }
2059
2646
  },
@@ -2061,14 +2648,26 @@
2061
2648
  toggleExpanded() {
2062
2649
  if (!this.isExpandable) return;
2063
2650
  this.isExpanded = !this.isExpanded;
2064
- if (this.isExpanded && this.isPartialDescription) {
2065
- messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2066
- }
2067
- }
2651
+ if (this.isExpanded && this.isPartialDescription) this.requestDescription();
2652
+ },
2653
+ requestDescription() {
2654
+ messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2655
+ },
2656
+ /**
2657
+ * Shows the menu for this value (eg. Copy as JSON).
2658
+ * @param {MouseEvent} evt
2659
+ */
2660
+ openMenu(evt) {
2661
+ this.$root.openValueMenu(evt, {
2662
+ path: this.path,
2663
+ keyPath: this.keyPath,
2664
+ isPrimitive: this.description.isPrimitive,
2665
+ });
2666
+ },
2068
2667
  },
2069
2668
  template: `
2070
2669
  <div :class="classNames">
2071
- <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
2670
+ <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded" @contextmenu.prevent.stop="openMenu"><span
2072
2671
  v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
2073
2672
  v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
2074
2673
  v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
@@ -2081,6 +2680,7 @@
2081
2680
  :name="entry[0]"
2082
2681
  :is-dim-name="group.isDim(entry)"
2083
2682
  :description="entry[1]"
2683
+ :key-path="group.getKeyPath(entry)"
2084
2684
  :path="path.concat([group.key, entryIndex])">
2085
2685
  </js-value>
2086
2686
  </template>
@@ -2184,30 +2784,113 @@
2184
2784
  }
2185
2785
 
2186
2786
  /**
2187
- * Finds the last statement in the code if it is an expression so that its
2188
- * value can be shown.
2787
+ * Gets a block of code ready to run: packages imported by name (eg.
2788
+ * `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
2789
+ * and the last statement is found if it is an expression so that its value
2790
+ * can be shown.
2189
2791
  * @param {string} code
2190
- * @param {"classic"|"module"} blockType
2191
- * @returns {[number, number]|null}
2192
- * The start and end index of the expression or `null` if the last statement
2193
- * isn't an expression (or the code can't be parsed, in which case running
2194
- * it will show the syntax error).
2792
+ * @param {Object} options
2793
+ * @param {"classic"|"module"} options.blockType
2794
+ * @param {string?} options.importsUrl
2795
+ * The URL template where `{specifier}` is replaced with what was imported
2796
+ * or an empty value to leave imports alone.
2797
+ * @param {boolean} options.findResult
2798
+ * @returns {{code: string, resultRange: [number, number]|null}}
2799
+ * The code to run and the start and end index of its last expression (or
2800
+ * `null`). If the code can't be parsed it is returned as is so that running
2801
+ * it shows the syntax error.
2195
2802
  */
2196
- function findLastExpression(code, blockType) {
2197
- if (!window.acorn) return null;
2803
+ function prepareCode(code, {blockType, importsUrl, findResult}) {
2804
+ if (!window.acorn) return {code, resultRange: null};
2805
+ let ast;
2198
2806
  try {
2199
- const {body} = acorn.parse(code, {
2807
+ ast = acorn.parse(code, {
2200
2808
  ecmaVersion: 'latest',
2201
2809
  sourceType: blockType === 'module' ? 'module' : 'script',
2202
2810
  allowHashBang: true,
2203
2811
  });
2204
- const last = body[body.length - 1];
2812
+ }
2813
+ catch (e) {
2814
+ return {code, resultRange: null};
2815
+ }
2816
+
2817
+ let resultRange = null;
2818
+ if (findResult) {
2819
+ const last = ast.body[ast.body.length - 1];
2205
2820
  if (last?.type === 'ExpressionStatement' && !last.directive) {
2206
- return [last.expression.start, last.expression.end];
2821
+ resultRange = [last.expression.start, last.expression.end];
2822
+ }
2823
+ }
2824
+
2825
+ // Finds every import (and re-export) of a package by name.
2826
+ const replacements = [];
2827
+ if (importsUrl) {
2828
+ walkAst(ast, node => {
2829
+ const {source} = /^(Import(Declaration|Expression)|Export(All|Named)Declaration)$/.test(node.type) ? node : {};
2830
+ if (source?.type === 'Literal' && 'string' === typeof source.value && isBareSpecifier(source.value)) {
2831
+ replacements.push({
2832
+ start: source.start,
2833
+ end: source.end,
2834
+ text: JSON.stringify(importsUrl.replace(/\{specifier\}/g, source.value)),
2835
+ });
2836
+ }
2837
+ });
2838
+ }
2839
+
2840
+ // Replaces them from last to first, moving the result's range as needed.
2841
+ replacements.sort((a, b) => b.start - a.start);
2842
+ for (const {start, end, text} of replacements) {
2843
+ code = code.slice(0, start) + text + code.slice(end);
2844
+ const shift = text.length - (end - start);
2845
+ if (resultRange) resultRange = resultRange.map(index => index >= end ? index + shift : index);
2846
+ }
2847
+
2848
+ return {code, resultRange};
2849
+ }
2850
+
2851
+ /**
2852
+ * Turns property keys into a path like the browser's console's "Copy property
2853
+ * path" (eg. ["people", "0", "first name"] becomes `people[0]["first name"]`).
2854
+ * @param {string[]} keys
2855
+ * @returns {string}
2856
+ */
2857
+ function formatPropertyPath(keys) {
2858
+ return keys.map((key, index) =>
2859
+ /^(0|[1-9]\d*)$/.test(key)
2860
+ ? `[${key}]`
2861
+ : /^[A-Za-z_$][\w$]*$/.test(key)
2862
+ ? (index ? '.' : '') + key
2863
+ : `[${JSON.stringify(key)}]`
2864
+ ).join('');
2865
+ }
2866
+
2867
+ /**
2868
+ * Indicates if an import specifier is a package name (eg. "lodash",
2869
+ * "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
2870
+ * "./utils.js", "https://example.com/x.js" or "node:fs").
2871
+ * @param {string} specifier
2872
+ * @returns {boolean}
2873
+ */
2874
+ function isBareSpecifier(specifier) {
2875
+ return !/^(\.{0,2}\/|#|[a-z][a-z\d+.-]*:)/i.test(specifier);
2876
+ }
2877
+
2878
+ /**
2879
+ * Calls the callback for every node in an Acorn syntax tree.
2880
+ * @param {*} node
2881
+ * @param {(node: *) => void} callback
2882
+ */
2883
+ function walkAst(node, callback) {
2884
+ if (Array.isArray(node)) {
2885
+ for (const child of node) walkAst(child, callback);
2886
+ }
2887
+ else if (node && 'string' === typeof node.type) {
2888
+ callback(node);
2889
+ for (const key of Object.keys(node)) {
2890
+ const value = node[key];
2891
+ if (value && 'object' === typeof value) walkAst(value, callback);
2207
2892
  }
2208
2893
  }
2209
- catch (e) {}
2210
- return null;
2211
2894
  }
2212
2895
 
2213
2896
  /**
@@ -2339,6 +3022,7 @@
2339
3022
  `${Object(part)[1]}`,
2340
3023
  ]),
2341
3024
  ...sanitizeEntries(description),
3025
+ ...(description.entriesArePropertyKeys != null ? {entriesArePropertyKeys: !!description.entriesArePropertyKeys} : {}),
2342
3026
  };
2343
3027
  }
2344
3028
 
@@ -2429,7 +3113,47 @@
2429
3113
  level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
2430
3114
  }
2431
3115
 
2432
- if (level) Object.assign(level, sanitizeEntries(description));
3116
+ if (!level) return;
3117
+ Object.assign(level, sanitizeEntries(description));
3118
+ level.entriesArePropertyKeys = !!Object(description).entriesArePropertyKeys;
3119
+ // A refresh also sends the value's preview.
3120
+ if (Array.isArray(Object(description).parts)) {
3121
+ const {typeName, isPrimitive, parts} = sanitizeDescription(description);
3122
+ Object.assign(level, {typeName, isPrimitive, parts});
3123
+ }
3124
+ }
3125
+
3126
+ /**
3127
+ * NOTE: Called via main by the runner with a value converted to JSON (see
3128
+ * requestValueJson()).
3129
+ * @param {{requestId: string, json?: string, circularCount?: number, error?: string}} result
3130
+ */
3131
+ function onValueJson(result) {
3132
+ const {requestId, json, circularCount, error} = Object(result);
3133
+ const resolve = jsonRequests.get(`${requestId}`);
3134
+ if (!resolve) return;
3135
+ jsonRequests.delete(`${requestId}`);
3136
+ resolve({
3137
+ json: json != null ? `${json}` : null,
3138
+ circularCount: +circularCount || 0,
3139
+ error: error != null ? `${error}` : null,
3140
+ });
3141
+ }
3142
+
3143
+ /**
3144
+ * The functions waiting for values to be converted to JSON by their request
3145
+ * IDs.
3146
+ * @type {Map<string, (result: {json: string?, circularCount: number, error: string?}) => void>}
3147
+ */
3148
+ const jsonRequests = new Map();
3149
+
3150
+ /**
3151
+ * NOTE: Called via main by the runner after a value was stored as a global
3152
+ * variable.
3153
+ * @param {string} name
3154
+ */
3155
+ function onStoredAsGlobal(name) {
3156
+ mountedApp.displays.push({type: 'notice', message: `Stored as the global variable ${`${name}`}`});
2433
3157
  }
2434
3158
 
2435
3159
  /**
@@ -2496,9 +3220,9 @@
2496
3220
  libraryUrl('prismjs', 'components/prism-core.min.js'),
2497
3221
  libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
2498
3222
  libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
2499
- // Used to find the last expression in each block of code so that its
2500
- // value can be shown.
2501
- ...(showResults ? [libraryUrl('acorn', 'dist/acorn.js')] : []),
3223
+ // Used to find the last expression in each block of code (so that its
3224
+ // value can be shown) and the packages that it imports.
3225
+ libraryUrl('acorn', 'dist/acorn.js'),
2502
3226
  ],
2503
3227
  cssUrls: [
2504
3228
  'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
@@ -2518,6 +3242,7 @@
2518
3242
  runnerMode,
2519
3243
  blockType,
2520
3244
  showResults,
3245
+ importsUrl,
2521
3246
  packageInfo: PACKAGE_INFO,
2522
3247
  libraryVersions: LIBRARY_VERSIONS,
2523
3248
  isPopOut: !!targetWindow,
@@ -2592,7 +3317,7 @@
2592
3317
  * The options of YourJSBox.create() which are the same as the data
2593
3318
  * attributes of a script tag.
2594
3319
  */
2595
- const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'librariesUrl', 'runner', 'showResults', 'theme'];
3320
+ const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
2596
3321
 
2597
3322
  /**
2598
3323
  * The JavaScript API for creating consoles (available as window.YourJSBox).
@@ -2623,6 +3348,7 @@
2623
3348
  * @param {string=} options.dividerOrient
2624
3349
  * @param {string=} options.theme
2625
3350
  * @param {string=} options.librariesUrl
3351
+ * @param {string=} options.importsUrl
2626
3352
  * The same as the data attributes of a script tag.
2627
3353
  * @returns {{element: HTMLIFrameElement, destroy: () => void}}
2628
3354
  */