yourjs-box 1.5.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.5.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}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
7
+ const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.history-panel{max-width:min(420px,calc(100% - 12px));width:360px}.history-title{color:var(--muted-text);font-size:11px;font-weight:600;letter-spacing:.05em;padding:4px 8px 6px;text-transform:uppercase}.history-item{justify-content:space-between}.history-label{font-family:var(--console-font);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-detail{color:var(--muted-text);flex-shrink:0;font-size:11px}.history-empty,.history-hint{color:var(--muted-text);font-size:12px;padding:6px 8px}.history-hint{border-top:1px solid var(--toolbar-border);margin-top:4px}.value-menu{bottom:auto;min-width:220px;position:fixed;right:auto;z-index:850}.toast{animation:fade-in .15s ease-out;background-color:var(--console-text);border-radius:6px;bottom:44px;box-shadow:0 6px 20px rgb(0 0 0 / .25);color:var(--console-bg);font-size:12px;left:50%;max-width:calc(100% - 32px);padding:6px 12px;pointer-events:none;position:fixed;transform:translateX(-50%);z-index:860}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
8
8
  /**
9
9
  * Information about this package (eg. its version).
10
10
  * @type {{name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}
11
11
  */
12
- const PACKAGE_INFO = {"name":"yourjs-box","version":"1.5.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
12
+ const PACKAGE_INFO = {"name":"yourjs-box","version":"1.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><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.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.
@@ -89,8 +89,8 @@
89
89
  * to these calls.
90
90
  */
91
91
  const RELAYABLE_FUNCS = {
92
- viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
93
- runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
92
+ viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'onStoredAsGlobal', 'onValueJson', 'updateDescriptionFor'],
93
+ runner: ['clearLogs', 'getValueAsJson', 'refreshDescription', 'reset', 'runCode', 'sendDescriptionFor', 'storeAsGlobal'],
94
94
  // Things that the viewer (but never the runner) can ask this script to do.
95
95
  host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
96
96
  };
@@ -631,9 +631,9 @@
631
631
  const $entries = [];
632
632
  const protoEntries = [];
633
633
  const $protoEntries = [];
634
+ let wasIterated = false;
634
635
 
635
636
  if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
636
- let wasIterated = false;
637
637
  try {
638
638
  // Map
639
639
  if (typeName === 'Map') {
@@ -681,7 +681,9 @@
681
681
  }
682
682
  }
683
683
 
684
- 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};
685
687
  }
686
688
 
687
689
  // ['123', 0, 'entries', 0]
@@ -792,16 +794,25 @@
792
794
  * entry indices.
793
795
  */
794
796
  function sendDescriptionFor(path) {
795
- // Get the description of the desired value.
796
- let level = logArgsById;
797
- let pathPartIndex = 0;
798
- for (let pathPart of path) {
799
- if (pathPartIndex && pathPartIndex % 2 === 0) {
800
- pathPart = '$' + pathPart;
801
- }
802
- level = level?.[pathPart];
803
- pathPartIndex++;
804
- }
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);
805
816
  if (!level) return;
806
817
  const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
807
818
 
@@ -809,11 +820,95 @@
809
820
  Object.assign(level, description);
810
821
 
811
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
+ : {};
812
826
  send({
813
827
  target: 'viewer',
814
828
  func: 'updateDescriptionFor',
815
- 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];
816
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}`]});
817
912
  }
818
913
 
819
914
  /**
@@ -824,7 +919,7 @@
824
919
  groupIds = [];
825
920
  }
826
921
 
827
- return {clearLogs, destroy, runCode, sendDescriptionFor};
922
+ return {clearLogs, destroy, getValueAsJson, refreshDescription, runCode, sendDescriptionFor, storeAsGlobal};
828
923
  }
829
924
 
830
925
 
@@ -1199,6 +1294,18 @@
1199
1294
  dividerSize: '8px',
1200
1295
  tempDividerPct: null,
1201
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,
1202
1309
  textScale: loadTextScale(),
1203
1310
  isFullscreen: false,
1204
1311
  // Set when full screen isn't allowed so the console fills the page.
@@ -1321,11 +1428,23 @@
1321
1428
  bottomButtons() {
1322
1429
  const isFullscreen = this.isFullscreen || this.isMaximized;
1323
1430
  return [
1431
+ {
1432
+ iconName: 'more',
1433
+ title: 'More',
1434
+ className: 'more-button',
1435
+ callback() { this.toggleMenu(); },
1436
+ },
1324
1437
  {
1325
1438
  iconName: 'clear',
1326
1439
  title: 'Clear console',
1327
1440
  callback() { this.clearConsole(); },
1328
1441
  },
1442
+ {
1443
+ iconName: 'history',
1444
+ title: `History (${isMac ? '\u2325' : 'Alt+'}H)`,
1445
+ className: 'history-button',
1446
+ callback() { this.toggleHistory(); },
1447
+ },
1329
1448
  {
1330
1449
  iconName: 'horizontalView',
1331
1450
  title: 'Show the editor below the console',
@@ -1343,12 +1462,6 @@
1343
1462
  title: isFullscreen ? 'Exit full screen' : 'Full screen',
1344
1463
  callback() { this.toggleFullscreen(); },
1345
1464
  },
1346
- {
1347
- iconName: 'more',
1348
- title: 'More',
1349
- className: 'more-button',
1350
- callback() { this.toggleMenu(); },
1351
- },
1352
1465
  {
1353
1466
  iconName: this.runningCount ? 'spinner' : 'play',
1354
1467
  label: 'Run',
@@ -1359,6 +1472,20 @@
1359
1472
  },
1360
1473
  ].filter(btn => !btn.showIf || btn.showIf.call(this));
1361
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
+ },
1362
1489
  canShrinkText() {
1363
1490
  return this.textScale > TEXT_SCALES[0];
1364
1491
  },
@@ -1399,9 +1526,110 @@
1399
1526
  this.runGroup(jsCodeGroup0);
1400
1527
 
1401
1528
  // Remove the code that was run from the editor.
1402
- this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1403
-
1404
- this.runCount++;
1529
+ const remainingCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1530
+
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;
1405
1633
  },
1406
1634
  /**
1407
1635
  * Runs the next hidden group if all of the visible code that came
@@ -1429,6 +1657,12 @@
1429
1657
  });
1430
1658
  this.runningCount++;
1431
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
+ }
1432
1666
  const {code, resultRange} = prepareCode(group.lines, {
1433
1667
  blockType: this.blockType,
1434
1668
  importsUrl: this.importsUrl,
@@ -1470,6 +1704,7 @@
1470
1704
  this.runCount = 0;
1471
1705
  this.runningCount = 0;
1472
1706
  this.runHistory = [];
1707
+ this.stopNavigatingHistory();
1473
1708
  messageParent({target: 'runner', func: 'reset', args: []});
1474
1709
  this.runHiddenGroups();
1475
1710
  },
@@ -1552,6 +1787,7 @@
1552
1787
  });
1553
1788
  if (!isConfirmed) return;
1554
1789
  }
1790
+ this.stopNavigatingHistory();
1555
1791
  this.jsCode = code;
1556
1792
  },
1557
1793
  /**
@@ -1637,6 +1873,9 @@
1637
1873
  jsCode: this.jsCode,
1638
1874
  originalCode: this.originalCode,
1639
1875
  fileName: this.fileName,
1876
+ commandHistory: this.commandHistory,
1877
+ historyIndex: this.historyIndex,
1878
+ historyDraft: this.historyDraft,
1640
1879
  runHistory: this.runHistory,
1641
1880
  hiddenGroups: this.hiddenGroups,
1642
1881
  runCount: this.runCount,
@@ -1658,6 +1897,8 @@
1658
1897
  this.closeMenu();
1659
1898
  }
1660
1899
  else {
1900
+ this.isHistoryOpen = false;
1901
+ this.closeValueMenu();
1661
1902
  this.isMenuOpen = true;
1662
1903
  this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
1663
1904
  }
@@ -1670,14 +1911,21 @@
1670
1911
  * Lets the arrow keys move between the menu's items.
1671
1912
  * @param {KeyboardEvent} evt
1672
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
+ */
1673
1919
  onMenuKeyDown(evt) {
1674
1920
  if (evt.key === 'Escape') {
1675
1921
  evt.stopPropagation();
1676
- this.closeMenu(true);
1922
+ this.closeMenu(this.isMenuOpen);
1923
+ this.closeValueMenu();
1924
+ this.isHistoryOpen = false;
1677
1925
  }
1678
1926
  else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
1679
1927
  evt.preventDefault();
1680
- const items = [...this.$refs.moreMenu.querySelectorAll('.menu-item:not(:disabled)')];
1928
+ const items = [...evt.currentTarget.querySelectorAll('.menu-item:not(:disabled)')];
1681
1929
  const index = items.indexOf(document.activeElement);
1682
1930
  const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
1683
1931
  next?.focus();
@@ -1757,15 +2005,189 @@
1757
2005
  if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
1758
2006
  this.closeMenu();
1759
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
+ }
1760
2014
  },
1761
2015
  /**
1762
2016
  * @param {KeyboardEvent} evt
1763
2017
  */
1764
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
+ }
1765
2029
  if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
1766
2030
  this.setMaximized(false);
1767
2031
  }
1768
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
+ },
1769
2191
  closeDialog(isConfirmed) {
1770
2192
  const {dialog} = this;
1771
2193
  this.dialog = null;
@@ -1786,6 +2208,7 @@
1786
2208
  }
1787
2209
  },
1788
2210
  onDisplaysScroll() {
2211
+ this.closeValueMenu();
1789
2212
  const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
1790
2213
  this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
1791
2214
  },
@@ -1859,7 +2282,8 @@
1859
2282
  mounted() {
1860
2283
  addEventListener('mousemove', this.onWindowMouseMove);
1861
2284
  addEventListener('mousedown', this.onWindowMouseDown);
1862
- addEventListener('keydown', this.onWindowKeyDown);
2285
+ // Capturing lets shortcuts (eg. Alt+H) work before the editor sees them.
2286
+ addEventListener('keydown', this.onWindowKeyDown, true);
1863
2287
  addEventListener('resize', this.onWindowResize);
1864
2288
  addEventListener('mouseup', this.onWindowMouseUp);
1865
2289
  addEventListener('error', this.onWindowError);
@@ -2005,6 +2429,28 @@
2005
2429
  });
2006
2430
 
2007
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
+
2008
2454
  editor.textInput.getElement().addEventListener('keydown', (evt) => {
2009
2455
  const isComboKeyCode = evt.keyCode === 16 // SHIFT
2010
2456
  || evt.keyCode === 17 // CTRL
@@ -2109,6 +2555,10 @@
2109
2555
  // https://icon-sets.iconify.design/mdi/error-outline/
2110
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>',
2111
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>',
2112
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>',
2113
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>',
2114
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>',
@@ -2136,7 +2586,10 @@
2136
2586
 
2137
2587
  function getJSValueComponentProps() {
2138
2588
  return {
2139
- 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'],
2140
2593
  data() {
2141
2594
  return {
2142
2595
  isExpanded: false,
@@ -2148,7 +2601,12 @@
2148
2601
  if (newValue && !this.hasBeenExpanded) {
2149
2602
  this.hasBeenExpanded = newValue;
2150
2603
  }
2151
- }
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
+ },
2152
2610
  },
2153
2611
  computed: {
2154
2612
  isPartialDescription() {
@@ -2169,10 +2627,20 @@
2169
2627
  ];
2170
2628
  },
2171
2629
  entryGroups() {
2172
- // 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;
2173
2633
  return [
2174
- { key: 'entries', isDim: entry => entry[2] === false },
2175
- { 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
+ },
2176
2644
  ];
2177
2645
  }
2178
2646
  },
@@ -2180,14 +2648,26 @@
2180
2648
  toggleExpanded() {
2181
2649
  if (!this.isExpandable) return;
2182
2650
  this.isExpanded = !this.isExpanded;
2183
- if (this.isExpanded && this.isPartialDescription) {
2184
- messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2185
- }
2186
- }
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
+ },
2187
2667
  },
2188
2668
  template: `
2189
2669
  <div :class="classNames">
2190
- <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
2191
2671
  v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
2192
2672
  v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
2193
2673
  v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
@@ -2200,6 +2680,7 @@
2200
2680
  :name="entry[0]"
2201
2681
  :is-dim-name="group.isDim(entry)"
2202
2682
  :description="entry[1]"
2683
+ :key-path="group.getKeyPath(entry)"
2203
2684
  :path="path.concat([group.key, entryIndex])">
2204
2685
  </js-value>
2205
2686
  </template>
@@ -2367,6 +2848,22 @@
2367
2848
  return {code, resultRange};
2368
2849
  }
2369
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
+
2370
2867
  /**
2371
2868
  * Indicates if an import specifier is a package name (eg. "lodash",
2372
2869
  * "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
@@ -2525,6 +3022,7 @@
2525
3022
  `${Object(part)[1]}`,
2526
3023
  ]),
2527
3024
  ...sanitizeEntries(description),
3025
+ ...(description.entriesArePropertyKeys != null ? {entriesArePropertyKeys: !!description.entriesArePropertyKeys} : {}),
2528
3026
  };
2529
3027
  }
2530
3028
 
@@ -2615,7 +3113,47 @@
2615
3113
  level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
2616
3114
  }
2617
3115
 
2618
- 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}`}`});
2619
3157
  }
2620
3158
 
2621
3159
  /**