yourjs-box 1.1.2 → 1.2.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.1.2 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.2.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
2
2
  (() => {
3
3
  /**
4
4
  * Viewer IFRAME's CSS code
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}@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:12px;line-height:16px}.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)}.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:12px!important;line-height:16px!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}@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:12px;line-height:16px}.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:12px!important;line-height:16px!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.1.2","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.2.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></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log'\" :class=\"['console-row', 'log-' + display.key]\" :title=\"display.name\"><span v-if=\"display.key === 'error'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></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></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\" @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=\"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>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</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue, Ace and Prism</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" @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=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
18
18
  /**
19
19
  * The URL of this script which is used to remove this script's lines from the
20
20
  * stack traces of errors in window mode.
@@ -28,6 +28,40 @@
28
28
  */
29
29
  const ORIGINAL_CONSOLE = window[Symbol.for('yourjs-box.originalConsole')] ??= {...console};
30
30
 
31
+ /**
32
+ * The libraries that the viewer loads along with the exact versions that it
33
+ * was tested with.
34
+ */
35
+ const LIBRARY_VERSIONS = {
36
+ 'ace-builds': '1.44.0',
37
+ 'acorn': '8.18.0',
38
+ 'prism-themes': '1.9.0',
39
+ 'prismjs': '1.30.0',
40
+ 'vue': '3.5.43',
41
+ };
42
+
43
+ /**
44
+ * Where the libraries are loaded from unless data-libraries-url is given.
45
+ * `{name}` and `{version}` are replaced with each library's name and version.
46
+ */
47
+ const DEFAULT_LIBRARIES_URL = 'https://unpkg.com/{name}@{version}/';
48
+
49
+ /**
50
+ * @param {string} librariesUrl
51
+ * The URL template (see DEFAULT_LIBRARIES_URL). Relative URLs are relative
52
+ * to the page.
53
+ * @param {keyof LIBRARY_VERSIONS} name
54
+ * @param {string} path
55
+ * The path of the file within the library's package.
56
+ * @returns {string}
57
+ */
58
+ function getLibraryFileUrl(librariesUrl, name, path) {
59
+ const baseUrl = librariesUrl
60
+ .replace(/\{(name|version)\}/g, (_, key) => key === 'name' ? name : LIBRARY_VERSIONS[name])
61
+ .replace(/\/?$/, '/');
62
+ return new URL(baseUrl + path, document.baseURI).href;
63
+ }
64
+
31
65
  /**
32
66
  * The only functions that may be relayed to the viewer and to the runner.
33
67
  * The runner executes the user's code so anything it sends must be limited
@@ -76,42 +110,143 @@
76
110
  let logCount = 0;
77
111
  let snippetCount = 0;
78
112
  let canImportDataUrls = true;
113
+ // The type of the block of code that is running (see runCode()).
114
+ let runningBlockType;
115
+ // How wrapping the last expression of each block shifted the columns on its
116
+ // line so that stack traces can show the original columns.
117
+ const columnShiftsBySnippet = {};
118
+
119
+ const counts = {};
120
+ const timers = {};
121
+ // The IDs of the groups (from console.group()) that are currently open.
122
+ let groupIds = [];
123
+ let groupCount = 0;
124
+
125
+ // Called by the code that is run (see runCode()) with the value of the last
126
+ // expression. The key is unique to this runner because in window mode more
127
+ // than one console can be on the same page.
128
+ const RESULT_KEY = `yourjs-box.result.${Math.random().toString(36).slice(2)}`;
129
+ globalThis[Symbol.for(RESULT_KEY)] = reportResult;
79
130
 
80
131
  // Overrides for console functions. In window mode this also captures
81
132
  // anything else that the page logs, just like the browser's console.
82
- for (const key of ['clear', 'debug', 'error', 'info', 'log', 'table', 'warn']) {
133
+ for (const key of [
134
+ 'assert', 'clear', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error',
135
+ 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'table', 'time',
136
+ 'timeEnd', 'timeLog', 'trace', 'warn',
137
+ ]) {
83
138
  const original = console[key];
84
139
  if ('function' !== typeof original) continue;
85
140
  console[key] = function(...args) {
86
- if (key === 'clear') {
87
- clearLogs();
88
- send({target: 'viewer', func: 'clearDisplays', args: [true]});
89
- }
90
- else {
91
- const table = key === 'table' ? parseTable(args[0], args[1]) : null;
141
+ handleConsoleCall(key, args);
92
142
 
93
- // console.table() only shows the data that was tabulated.
94
- if (table) args = args.slice(0, 1);
143
+ // Calls and returns the original console function.
144
+ return original.apply(this, arguments);
145
+ };
146
+ }
95
147
 
96
- // Keep track of the args so that they can be expanded later.
97
- const logId = `${++logCount}`;
98
- logArgsById[logId] = args.map(value => ({...summarize(value), value}));
148
+ /**
149
+ * Shows what was passed to a console function in the same way that the
150
+ * browser's console would.
151
+ * @param {string} key
152
+ * @param {any[]} args
153
+ */
154
+ function handleConsoleCall(key, args) {
155
+ const label = args.length && args[0] !== undefined ? `${args[0]}` : 'default';
99
156
 
100
- send({
101
- target: 'viewer',
102
- func: 'appendLog',
103
- args: [{
104
- logId,
105
- key,
106
- descriptions: logArgsById[logId].map(without(['value'])),
107
- table,
108
- }]
109
- });
157
+ if (key === 'clear') {
158
+ clearLogs();
159
+ groupIds = [];
160
+ send({target: 'viewer', func: 'clearDisplays', args: [true]});
161
+ }
162
+ else if (key === 'assert') {
163
+ if (args[0]) return;
164
+ const rest = args.slice(1);
165
+ addLog('assert', !rest.length
166
+ ? ['Assertion failed: console.assert']
167
+ : 'string' === typeof rest[0]
168
+ ? ['Assertion failed: ' + rest[0], ...rest.slice(1)]
169
+ : ['Assertion failed:', ...rest]);
170
+ }
171
+ else if (key === 'count') {
172
+ counts[label] = (counts[label] || 0) + 1;
173
+ addLog('count', [`${label}: ${counts[label]}`]);
174
+ }
175
+ else if (key === 'countReset') {
176
+ if (Object.hasOwn(counts, label)) counts[label] = 0;
177
+ else addLog('warn', [`Count for '${label}' does not exist`]);
178
+ }
179
+ else if (key === 'time') {
180
+ if (Object.hasOwn(timers, label)) addLog('warn', [`Timer '${label}' already exists`]);
181
+ else timers[label] = performance.now();
182
+ }
183
+ else if (key === 'timeLog' || key === 'timeEnd') {
184
+ if (!Object.hasOwn(timers, label)) {
185
+ addLog('warn', [`Timer '${label}' does not exist`]);
186
+ return;
110
187
  }
188
+ const ms = performance.now() - timers[label];
189
+ if (key === 'timeEnd') delete timers[label];
190
+ addLog(key, [`${label}: ${+ms.toFixed(3)} ms`, ...(key === 'timeLog' ? args.slice(1) : [])]);
191
+ }
192
+ else if (key === 'trace') {
193
+ // Removes the first line ("Error") and this runner's own lines.
194
+ const stack = cleanStack(new Error().stack ?? '').split('\n').slice(1).join('\n');
195
+ addLog('trace', args.length ? args : ['console.trace'], {stack});
196
+ }
197
+ else if (key === 'group' || key === 'groupCollapsed') {
198
+ const groupId = `${++groupCount}`;
199
+ addLog(key, args.length ? args : ['console.group'], {groupId, isCollapsed: key === 'groupCollapsed'});
200
+ groupIds.push(groupId);
201
+ }
202
+ else if (key === 'groupEnd') {
203
+ groupIds.pop();
204
+ }
205
+ else {
206
+ const table = key === 'table' ? parseTable(args[0], args[1]) : null;
111
207
 
112
- // Calls and returns the original console function.
113
- return original.apply(this, arguments);
114
- };
208
+ // console.table() only shows the data that was tabulated.
209
+ addLog(key, table ? args.slice(0, 1) : args, {table});
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Sends a message to show in the console.
215
+ * @param {string} key
216
+ * The name of the console function (eg. "log").
217
+ * @param {any[]} args
218
+ * @param {Object=} extra
219
+ * Other properties to send along with the message.
220
+ * @param {number=} depth
221
+ * Optional, defaults to `0`. The depth used to summarize the args (see
222
+ * summarize()).
223
+ */
224
+ function addLog(key, args, extra, depth = 0) {
225
+ // Keep track of the args so that they can be expanded later.
226
+ const logId = `${++logCount}`;
227
+ logArgsById[logId] = args.map(value => ({...summarize(value, depth), value}));
228
+
229
+ send({
230
+ target: 'viewer',
231
+ func: 'appendLog',
232
+ args: [{
233
+ logId,
234
+ key,
235
+ descriptions: logArgsById[logId].map(without(['value'])),
236
+ groupIds: groupIds.slice(),
237
+ ...extra,
238
+ }]
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Shows the value of the last expression in the code that was run unless
244
+ * it is `undefined`.
245
+ * @param {*} value
246
+ */
247
+ function reportResult(value) {
248
+ // Like the browser, strings are quoted (but logged strings aren't).
249
+ if (value !== undefined) addLog('result', [value], {}, 'string' === typeof value ? 1 : 0);
115
250
  }
116
251
 
117
252
  addEventListener('error', evt => {
@@ -127,12 +262,17 @@
127
262
  * @param {boolean=} isInPromise
128
263
  */
129
264
  function reportUncaught(error, isInPromise) {
265
+ // Points out how to use top-level await if it was used in a classic block.
266
+ const hint = runningBlockType === 'classic' && error?.name === 'SyntaxError' && /\bawait\b/.test(error.message)
267
+ ? '\n(To use top-level await, add data-block-type="module" to the script tag.)'
268
+ : '';
130
269
  send({
131
270
  target: 'viewer',
132
271
  func: 'appendError',
133
272
  args: [{
134
273
  message: (isInPromise ? 'Uncaught (in promise) ' : 'Uncaught ')
135
- + cleanStack(error?.stack ?? `${error?.message ?? error}`),
274
+ + cleanStack(error?.stack ?? `${error?.message ?? error}`)
275
+ + hint,
136
276
  }]
137
277
  });
138
278
  }
@@ -147,7 +287,14 @@
147
287
  return `${stack}`
148
288
  .split('\n')
149
289
  .filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
150
- .join('\n');
290
+ .join('\n')
291
+ // Undoes the shift in columns caused by wrapping the last expression.
292
+ .replace(/(snippet-\d+\.js):(\d+):(\d+)/g, (match, name, line, column) => {
293
+ const info = columnShiftsBySnippet[name];
294
+ return info && +line === info.line && +column > info.column
295
+ ? `${name}:${line}:${Math.max(info.column, column - info.shift)}`
296
+ : match;
297
+ });
151
298
  }
152
299
 
153
300
  function without(props, obj) {
@@ -278,7 +425,7 @@
278
425
  function getPreviewParts(value, depth, typeName = getTypeName(value)) {
279
426
  if (typeName === 'string') {
280
427
  if (!depth) return [['text', value]];
281
- if (depth > 1 && value.length > 100) value = value.slice(0, 99) + '…';
428
+ if (depth > 1 && value.length > 100) value = value.slice(0, 99) + '\u2026';
282
429
  return [['string', quote(value)]];
283
430
  }
284
431
  if (typeName === 'number') return [['number', Object.is(value, -0) ? '-0' : '' + value]];
@@ -325,10 +472,10 @@
325
472
  // Like the browser, show the source of functions passed directly to a
326
473
  // console function.
327
474
  if (!depth) {
328
- return [['text', isClass ? source : source.replace(/^(async\s+)?function\b\s*/, '$1ƒ ')]];
475
+ return [['text', isClass ? source : source.replace(/^(async\s+)?function\b\s*/, '$1\u0192 ')]];
329
476
  }
330
- if (depth > 1) return [['function', 'ƒ']];
331
- return [['function', isClass ? `class ${value.name}` : `ƒ ${value.name}()`]];
477
+ if (depth > 1) return [['function', '\u0192']];
478
+ return [['function', isClass ? `class ${value.name}` : `\u0192 ${value.name}()`]];
332
479
  }
333
480
 
334
481
  /**
@@ -345,6 +492,8 @@
345
492
  return [['text', isNaN(value) ? 'Invalid Date' : Date.prototype.toString.call(value)]];
346
493
  }
347
494
  if (typeName === 'RegExp') return [['regexp', '' + value]];
495
+ // The state of a promise can't be determined synchronously.
496
+ if (typeName === 'Promise') return [['text', `${className} {\u2026}`]];
348
497
  // DOM nodes (only available in window mode) are shown like the browser
349
498
  // shows them in previews (eg. "h2#title.big").
350
499
  if ('undefined' !== typeof Node && value instanceof Node) {
@@ -364,7 +513,7 @@
364
513
  if (depth > 1) {
365
514
  if (isArrayLike) return [['text', `${className}(${value.length})`]];
366
515
  if (typeName === 'Map' || typeName === 'Set') return [['text', `${className}(${value.size})`]];
367
- return [['text', className === 'Object' ? '{…}' : className]];
516
+ return [['text', className === 'Object' ? '{\u2026}' : className]];
368
517
  }
369
518
 
370
519
  const parts = [];
@@ -373,7 +522,7 @@
373
522
  if (index) parts.push(['text', ', ']);
374
523
  addItem(item);
375
524
  });
376
- if (total > items.length) parts.push(['text', (items.length ? ', ' : '') + '…']);
525
+ if (total > items.length) parts.push(['text', (items.length ? ', ' : '') + '\u2026']);
377
526
  };
378
527
 
379
528
  if (isArrayLike) {
@@ -413,7 +562,7 @@
413
562
  parts.push(
414
563
  ['key', key],
415
564
  ['text', ': '],
416
- ...('value' in descriptor ? getPreviewParts(descriptor.value, 2) : [['text', '(…)']])
565
+ ...('value' in descriptor ? getPreviewParts(descriptor.value, 2) : [['text', '(\u2026)']])
417
566
  );
418
567
  });
419
568
  parts.push(['text', '}']);
@@ -501,30 +650,64 @@
501
650
  // }
502
651
 
503
652
  /**
504
- * Runs the code as a classic script so that top-level declarations are
505
- * shared between all of the code that is run.
653
+ * Runs a block of code.
506
654
  * @param {string} jsCode
655
+ * @param {Object=} runOptions
656
+ * @param {"classic"|"module"=} runOptions.blockType
657
+ * "classic" (the default) runs the code as a classic script so that
658
+ * top-level declarations are shared between all of the code that is run.
659
+ * "module" runs the code as a module which allows for top-level await and
660
+ * import statements, but top-level declarations stay in the module.
661
+ * @param {[number, number]=} runOptions.resultRange
662
+ * The start and end index of the last expression in the code whose value
663
+ * should be shown.
507
664
  */
508
- function runCode(jsCode) {
665
+ async function runCode(jsCode, runOptions) {
666
+ const {blockType, resultRange} = Object(runOptions);
667
+
509
668
  // Names the code so that stack traces refer to it by this name.
510
- const source = `${jsCode}\n//# sourceURL=snippet-${++snippetCount}.js`;
669
+ const snippetName = `snippet-${++snippetCount}.js`;
511
670
 
512
- if (mode === 'worker') {
513
- try {
671
+ // Wraps the last expression so that its value is reported. This keeps
672
+ // everything on the same lines so that line numbers in errors still match
673
+ // and the shift in columns is recorded so they can be fixed too.
674
+ if (Array.isArray(resultRange)) {
675
+ const [start, end] = resultRange;
676
+ const prefix = `globalThis[Symbol.for(${JSON.stringify(RESULT_KEY)})]((`;
677
+ const linesBefore = jsCode.slice(0, start).split('\n');
678
+ columnShiftsBySnippet[snippetName] = {
679
+ line: linesBefore.length,
680
+ column: linesBefore[linesBefore.length - 1].length + 1,
681
+ shift: prefix.length,
682
+ };
683
+ jsCode = jsCode.slice(0, start) + prefix + jsCode.slice(start, end) + '))' + jsCode.slice(end);
684
+ }
685
+
686
+ const source = `${jsCode}\n//# sourceURL=${snippetName}`;
687
+
688
+ runningBlockType = blockType === 'module' ? 'module' : 'classic';
689
+ try {
690
+ if (blockType === 'module') {
691
+ // Waits for the module to finish (including any top-level await).
692
+ await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
693
+ }
694
+ else if (mode === 'worker') {
514
695
  importCode(source);
515
696
  }
516
- catch (e) {
517
- reportUncaught(e);
697
+ else {
698
+ const script = document.createElement('script');
699
+ script.textContent = source;
700
+ document.head.appendChild(script);
701
+ script.remove();
518
702
  }
519
703
  }
520
- else {
521
- const script = document.createElement('script');
522
- script.textContent = source;
523
- document.head.appendChild(script);
524
- script.remove();
704
+ catch (e) {
705
+ reportUncaught(e);
525
706
  }
707
+ runningBlockType = undefined;
526
708
 
527
- // Lets the viewer know that any synchronous logs have already been sent.
709
+ // Lets the viewer know that the code finished running and that any logs
710
+ // made while it ran have already been sent.
528
711
  send({target: 'viewer', func: 'onCodeRan', args: []});
529
712
  }
530
713
 
@@ -592,6 +775,7 @@
592
775
  */
593
776
  function clearLogs() {
594
777
  for (const logId of Object.keys(logArgsById)) delete logArgsById[logId];
778
+ groupIds = [];
595
779
  }
596
780
 
597
781
  return {clearLogs, runCode, sendDescriptionFor};
@@ -705,6 +889,9 @@
705
889
  function main(script) {
706
890
  const dataset = JSON.parse(JSON.stringify(script.dataset));
707
891
  const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
892
+ const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
893
+ const showResults = dataset.showResults !== 'false';
894
+ const libraryUrl = getLibraryFileUrl.bind(null, dataset.librariesUrl || DEFAULT_LIBRARIES_URL);
708
895
 
709
896
  /** @type {ReturnType<createCallableFrame>} */
710
897
  let callableViewerFrame;
@@ -722,9 +909,18 @@
722
909
  jsCode() {
723
910
  let mountedApp;
724
911
 
912
+ /**
913
+ * Indicates if any of the libraries that the viewer needs failed to load (eg.
914
+ * because the CDN is blocked or the user is offline).
915
+ */
916
+ const IS_MISSING_LIBRARIES = !window.Vue || !window.ace || !window.Prism;
917
+ if (IS_MISSING_LIBRARIES) {
918
+ document.querySelector('#splash').classList.add('failed');
919
+ }
920
+
725
921
  const Prism = window.Prism;
726
922
  delete window.Prism;
727
- Prism.plugins.autoloader.loadLanguages('javascript');
923
+ Prism?.plugins.autoloader.loadLanguages('javascript');
728
924
 
729
925
  /**
730
926
  * The minimum amount of time (in milliseconds) that the loading screen is shown.
@@ -752,6 +948,8 @@
752
948
  * @param {{runnerMode: "worker"|"window", packageInfo: {name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}} meta
753
949
  */
754
950
  function init(jsCode, dataset, meta) {
951
+ if (IS_MISSING_LIBRARIES) return;
952
+
755
953
  const hidePrefix = dataset.hidePrefix ?? '';
756
954
  const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
757
955
  const originalCode = unindentMin(jsCode);
@@ -767,7 +965,10 @@
767
965
  hidePrefix,
768
966
  hiddenGroups: copyHiddenGroups(),
769
967
  runnerMode: meta.runnerMode,
968
+ blockType: meta.blockType,
969
+ showResults: meta.showResults,
770
970
  packageInfo: meta.packageInfo,
971
+ libraryVersions: meta.libraryVersions,
771
972
  // Every group of code that was run (in order) which is used when
772
973
  // copying the console as HTML. Unlike the displays this is only
773
974
  // cleared by a reset.
@@ -817,6 +1018,22 @@
817
1018
  const name = this.theme === 'dark' ? 'Dark' : 'Light';
818
1019
  return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
819
1020
  },
1021
+ blockTypeDescription() {
1022
+ return this.blockType === 'module'
1023
+ ? 'Modules (top-level await and imports work, declarations stay in each block)'
1024
+ : 'Classic scripts (top-level declarations are shared between blocks)';
1025
+ },
1026
+ /**
1027
+ * The IDs of the groups (from console.groupCollapsed() or a group that
1028
+ * the user collapsed) whose messages should be hidden.
1029
+ */
1030
+ collapsedGroupIds() {
1031
+ return new Set(
1032
+ this.displays
1033
+ .filter(d => d.groupId && d.isCollapsed)
1034
+ .map(d => d.groupId)
1035
+ );
1036
+ },
820
1037
  layoutDescription() {
821
1038
  return (this.dividerOrient === 'vertical' ? 'Editor beside the console' : 'Editor below the console')
822
1039
  + (this.isDividerOrientAuto ? ' (automatic)' : '');
@@ -864,10 +1081,19 @@
864
1081
  blocks.push(...pendingHiddenGroups.map(group => group.allLines));
865
1082
  return joinCodeBlocks(blocks);
866
1083
  },
1084
+ /**
1085
+ * The data attributes for the copied console. data-libraries-url is
1086
+ * left out because it usually points to files on this site while the
1087
+ * copied console loads JS Box from a CDN.
1088
+ */
1089
+ exportDataset() {
1090
+ const {librariesUrl, ...exportDataset} = dataset;
1091
+ return exportDataset;
1092
+ },
867
1093
  exportHtml() {
868
1094
  return buildConsoleHtml({
869
1095
  code: this.exportJsCode,
870
- dataset,
1096
+ dataset: this.exportDataset,
871
1097
  packageInfo: this.packageInfo,
872
1098
  isFullPage: this.exportFormat === 'page',
873
1099
  });
@@ -973,7 +1199,18 @@
973
1199
  });
974
1200
  this.runningCount++;
975
1201
  this.runHistory.push(group.allLines);
976
- messageParent({target: 'runner', func: 'runCode', args: [group.lines]});
1202
+ messageParent({
1203
+ target: 'runner',
1204
+ func: 'runCode',
1205
+ args: [group.lines, {
1206
+ blockType: this.blockType,
1207
+ // Like the browser's console, show the value of the last
1208
+ // expression (except for hidden code).
1209
+ resultRange: this.showResults && !isHidden
1210
+ ? findLastExpression(group.lines, this.blockType)
1211
+ : null,
1212
+ }],
1213
+ });
977
1214
  },
978
1215
  clearConsole() {
979
1216
  this.displays = [];
@@ -1062,7 +1299,7 @@
1062
1299
  downloadExport() {
1063
1300
  const html = buildConsoleHtml({
1064
1301
  code: this.exportJsCode,
1065
- dataset,
1302
+ dataset: this.exportDataset,
1066
1303
  packageInfo: this.packageInfo,
1067
1304
  isFullPage: true,
1068
1305
  });
@@ -1070,6 +1307,24 @@
1070
1307
  Object.assign(document.createElement('a'), {href: url, download: 'js-box.html'}).click();
1071
1308
  setTimeout(() => URL.revokeObjectURL(url), 1000);
1072
1309
  },
1310
+ /**
1311
+ * Clicking a group's header (from console.group()) collapses or expands
1312
+ * the group unless a value in the header was clicked.
1313
+ * @param {*} display
1314
+ * @param {MouseEvent} evt
1315
+ */
1316
+ onLogRowClick(display, evt) {
1317
+ if (display.groupId && !evt.target.closest('.js-value-header.expandable')) {
1318
+ display.isCollapsed = !display.isCollapsed;
1319
+ }
1320
+ },
1321
+ /**
1322
+ * Indicates if a display is inside of a collapsed group.
1323
+ */
1324
+ isInCollapsedGroup(display) {
1325
+ const {collapsedGroupIds} = this;
1326
+ return !!collapsedGroupIds.size && !!display.groupIds?.some(id => collapsedGroupIds.has(id));
1327
+ },
1073
1328
  closeDialog(isConfirmed) {
1074
1329
  const {dialog} = this;
1075
1330
  this.dialog = null;
@@ -1390,6 +1645,7 @@
1390
1645
  // https://icon-sets.iconify.design/mdi/error-outline/
1391
1646
  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>',
1392
1647
  // Icons similar to those in the browser's console.
1648
+ result: '<svg viewBox="0 0 16 16"><path d="M9 4.5L5.5 8 9 11.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="8" r="1.1" fill="currentColor"/></svg>',
1393
1649
  close: '<svg viewBox="0 0 16 16"><path d="M4 4l8 8m0-8l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',
1394
1650
  clear: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.75 12.25l8.5-8.5" stroke="currentColor" stroke-width="1.5"/></svg>',
1395
1651
  copyToEditor: '<svg viewBox="0 0 16 16"><path d="M6 3.5L2.5 7 6 10.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7h6.5a4 4 0 0 1 4 4v1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
@@ -1573,6 +1829,33 @@
1573
1829
  };
1574
1830
  }
1575
1831
 
1832
+ /**
1833
+ * Finds the last statement in the code if it is an expression so that its
1834
+ * value can be shown.
1835
+ * @param {string} code
1836
+ * @param {"classic"|"module"} blockType
1837
+ * @returns {[number, number]|null}
1838
+ * The start and end index of the expression or `null` if the last statement
1839
+ * isn't an expression (or the code can't be parsed, in which case running
1840
+ * it will show the syntax error).
1841
+ */
1842
+ function findLastExpression(code, blockType) {
1843
+ if (!window.acorn) return null;
1844
+ try {
1845
+ const {body} = acorn.parse(code, {
1846
+ ecmaVersion: 'latest',
1847
+ sourceType: blockType === 'module' ? 'module' : 'script',
1848
+ allowHashBang: true,
1849
+ });
1850
+ const last = body[body.length - 1];
1851
+ if (last?.type === 'ExpressionStatement' && !last.directive) {
1852
+ return [last.expression.start, last.expression.end];
1853
+ }
1854
+ }
1855
+ catch (e) {}
1856
+ return null;
1857
+ }
1858
+
1576
1859
  /**
1577
1860
  * Joins groups of code back together making sure that each one (other than
1578
1861
  * the first) starts with a header so that they stay separate groups.
@@ -1733,12 +2016,12 @@
1733
2016
  * @param {any[]} options.descriptions
1734
2017
  * @param {{headers: string[], rows: {index: string, cells: any[]}[]}|null} options.table
1735
2018
  */
1736
- function appendLog({logId, key, descriptions, table}) {
2019
+ function appendLog({logId, key, descriptions, table, groupIds, groupId, isCollapsed, stack}) {
1737
2020
  key = `${key}`;
1738
2021
  mountedApp.displays.push({
1739
2022
  type: 'log',
1740
2023
  key,
1741
- name: `console.${key}`,
2024
+ name: key === 'result' ? 'Result' : `console.${key}`,
1742
2025
  descriptions: Array.from(descriptions, sanitizeDescription),
1743
2026
  table: table ? {
1744
2027
  headers: Array.from(table.headers, h => `${h}`),
@@ -1747,6 +2030,12 @@
1747
2030
  cells: Array.from(row.cells, cell => cell && sanitizeDescription(cell)),
1748
2031
  })),
1749
2032
  } : null,
2033
+ // The groups (from console.group()) that this message is in.
2034
+ groupIds: Array.from(groupIds ?? [], id => `${id}`),
2035
+ // Set if this message starts a group.
2036
+ groupId: groupId != null ? `${groupId}` : null,
2037
+ isCollapsed: !!isCollapsed,
2038
+ stack: stack != null ? `${stack}` : null,
1750
2039
  logId: `${logId}`,
1751
2040
  });
1752
2041
  }
@@ -1814,17 +2103,23 @@
1814
2103
  }
1815
2104
 
1816
2105
  },
2106
+ // Exact versions are used so that a new release of a library can never
2107
+ // change how an existing version of this console works. Ace and Prism
2108
+ // load other files (eg. language modes) from next to these files.
1817
2109
  jsUrls: [
1818
- 'https://unpkg.com/vue@3/dist/vue.global.prod.js',
1819
- 'https://unpkg.com/ace-builds@1/src-noconflict/ace.js',
1820
- 'https://unpkg.com/prismjs@1/components/prism-core.min.js',
1821
- 'https://unpkg.com/prismjs@1/plugins/autoloader/prism-autoloader.min.js',
1822
- 'https://unpkg.com/prismjs@1/plugins/match-braces/prism-match-braces.min.js',
2110
+ libraryUrl('vue', 'dist/vue.global.prod.js'),
2111
+ libraryUrl('ace-builds', 'src-noconflict/ace.js'),
2112
+ libraryUrl('prismjs', 'components/prism-core.min.js'),
2113
+ libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
2114
+ libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
2115
+ // Used to find the last expression in each block of code so that its
2116
+ // value can be shown.
2117
+ ...(showResults ? [libraryUrl('acorn', 'dist/acorn.js')] : []),
1823
2118
  ],
1824
2119
  cssUrls: [
1825
2120
  'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
1826
- 'https://unpkg.com/prism-themes@1/themes/prism-vsc-dark-plus.min.css',
1827
- 'https://unpkg.com/prismjs@1/plugins/match-braces/prism-match-braces.min.css',
2121
+ libraryUrl('prism-themes', 'themes/prism-vsc-dark-plus.min.css'),
2122
+ libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.css'),
1828
2123
  ],
1829
2124
  htmlAttributes: 'data-theme="' + theme + '"',
1830
2125
  onMessage(message) {
@@ -1833,7 +2128,7 @@
1833
2128
  async onReady() {
1834
2129
  // The dataset is passed as is so that the viewer knows which options
1835
2130
  // were actually specified (eg. when copying the console as HTML).
1836
- this.call('init', script.textContent, dataset, {runnerMode, packageInfo: PACKAGE_INFO});
2131
+ this.call('init', script.textContent, dataset, {runnerMode, blockType, showResults, packageInfo: PACKAGE_INFO, libraryVersions: LIBRARY_VERSIONS});
1837
2132
  },
1838
2133
  body: VIEWER_IFRAME_HTML,
1839
2134
  style: {