yourjs-box 1.8.0 → 1.10.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.8.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.10.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}.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}";
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}.svg-defs{height:0;position:absolute;width:0}#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)}#main.is-output-hidden{grid-template-columns:1fr;grid-template-rows:1fr}#main.is-output-hidden>#displays,#main.is-output-hidden>.divider{display:none}.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{flex:none;height:18px;width:18px}.logo-text{font-size:14px;white-space:nowrap}@media (max-width:440px){#bottomNav .logo-text{display:none}}.logo-your{font-weight:400;opacity:.8}.logo-large{gap:12px}.logo-large>.logo-mark{filter:drop-shadow(0 6px 12px rgb(0 0 0 / .18));height:56px;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{height:40px;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.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-shortcut{color:var(--muted-text);font-size:11px;margin-left:auto;padding-left:16px}.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}.console-row>.row-icon.repeat-count{background-color:var(--null);border-radius:7px;box-sizing:border-box;color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-size:10px;height:14px;left:calc(3px + var(--depth,0) * 16px);line-height:14px;min-width:16px;padding:0 4px;text-align:center;top:3px}.log-warn>.row-icon.repeat-count{background-color:var(--warn-icon)}.log-assert>.row-icon.repeat-count,.log-error>.row-icon.repeat-count{background-color:var(--error-icon)}.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}.row-content>.js-value+.js-value.is-joined{margin-left:0}.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.8.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.10.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,.ts,.mts,.cts,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism :language=\"language\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" :language=\"language\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Language</dt><dd>{{ isTypeScript ? 'TypeScript (types are removed by Babel, not checked)' : 'JavaScript' }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with {{ builtWith }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<svg class=\"svg-defs\" aria-hidden=\"true\"><!-- The logo: an open box whose front is the yellow JS square. --><symbol id=\"logo-mark\" viewBox=\"0 0 32 32\"><polygon points=\"1,10 8,3 31,3 24,10\" fill=\"#4a4a4a\"/><polygon points=\"8,3 31,3 29,5 10,5\" fill=\"#2e2e2e\"/><polygon points=\"24,10 31,3 31,24 24,31\" fill=\"#c7b000\"/><rect x=\"1\" y=\"10\" width=\"23\" height=\"21\" rx=\"1.5\" fill=\"#f7df1e\"/><text x=\"22\" y=\"28.5\" text-anchor=\"end\" font-size=\"11.5\" font-weight=\"800\" letter-spacing=\"-0.3\" fill=\"#1a1a1a\">JS</text></symbol></svg><div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><svg class=\"logo-mark\"><use href=\"#logo-mark\"/></svg><span class=\"logo-text\"><span class=\"logo-your\">Your</span>JS 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\">YourJS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.ts,.mts,.cts,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism :language=\"language\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.count > 1\" class=\"row-icon repeat-count\">{{ display.count }}</span> <span v-else-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\" :class=\"description.isJoined ? 'is-joined' : ''\" :style=\"description.style\" :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 v-if=\"display.count > 1\" class=\"row-icon repeat-count\">{{ display.count }}</span> <span v-else class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" :language=\"language\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" :word-wrap=\"wordWrap\" :rulers=\"rulers\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About YourJS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><svg class=\"logo-mark\"><use href=\"#logo-mark\"/></svg><span class=\"logo-text\"><span class=\"logo-your\">Your</span>JS 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\"><button class=\"menu-item\" role=\"menuitem\" :disabled=\"!canRunCode\" @click=\"closeMenu(); runAll()\"><span class=\"menu-icon\"><icon name=\"runAll\"></icon></span>Run all blocks<span class=\"menu-shortcut\">{{ modKey === 'Ctrl' ? 'Ctrl+Shift+Enter' : '&#8679;&#8984;&#8629;' }}</span></button><div class=\"menu-separator\"></div><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 YourJS 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\"><svg class=\"logo-mark\"><use href=\"#logo-mark\"/></svg><span class=\"logo-text\"><span class=\"logo-your\">Your</span>JS 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 YourJS 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\"><svg class=\"logo-mark\"><use href=\"#logo-mark\"/></svg><span class=\"logo-text\"><span class=\"logo-your\">Your</span>JS Box</span></span><div><div class=\"about-name\">YourJS Box</div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Language</dt><dd>{{ isTypeScript ? 'TypeScript (types are removed by Babel, not checked)' : 'JavaScript' }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>{{ modKey }}</kbd> + <kbd>Shift</kbd> + <kbd>Enter</kbd></dt><dd>Run all of the remaining blocks</dd><dt><kbd>Ctrl</kbd> + <kbd>L</kbd></dt><dd>Clear the console</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with {{ builtWith }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
18
18
  /**
19
19
  * The URL of this script which is used to remove this script's lines from the
20
20
  * stack traces of errors in window mode.
@@ -195,7 +195,7 @@
195
195
  else if (key === 'assert') {
196
196
  if (args[0]) return;
197
197
  const rest = args.slice(1);
198
- addLog('assert', !rest.length
198
+ addFormattedLog('assert', !rest.length
199
199
  ? ['Assertion failed: console.assert']
200
200
  : 'string' === typeof rest[0]
201
201
  ? ['Assertion failed: ' + rest[0], ...rest.slice(1)]
@@ -225,22 +225,109 @@
225
225
  else if (key === 'trace') {
226
226
  // Removes the first line ("Error") and this runner's own lines.
227
227
  const stack = cleanStack(new Error().stack ?? '').split('\n').slice(1).join('\n');
228
- addLog('trace', args.length ? args : ['console.trace'], {stack});
228
+ addFormattedLog('trace', args.length ? args : ['console.trace'], {stack});
229
229
  }
230
230
  else if (key === 'group' || key === 'groupCollapsed') {
231
231
  const groupId = `${++groupCount}`;
232
- addLog(key, args.length ? args : ['console.group'], {groupId, isCollapsed: key === 'groupCollapsed'});
232
+ addFormattedLog(key, args.length ? args : ['console.group'], {groupId, isCollapsed: key === 'groupCollapsed'});
233
233
  groupIds.push(groupId);
234
234
  }
235
235
  else if (key === 'groupEnd') {
236
236
  groupIds.pop();
237
237
  }
238
- else {
238
+ else if (key === 'table' || key === 'dir' || key === 'dirxml') {
239
239
  const table = key === 'table' ? parseTable(args[0], args[1]) : null;
240
240
 
241
241
  // console.table() only shows the data that was tabulated.
242
242
  addLog(key, table ? args.slice(0, 1) : args, {table});
243
243
  }
244
+ else {
245
+ addFormattedLog(key, args);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Like addLog() but if the first argument is a string, the format
251
+ * specifiers in it (eg. "%s" or "%c") are replaced like the browser's
252
+ * console does.
253
+ * @param {string} key
254
+ * @param {any[]} args
255
+ * @param {Object=} extra
256
+ */
257
+ function addFormattedLog(key, args, extra) {
258
+ const {values, formats} = formatArgs(args);
259
+ addLog(key, values, extra, 0, formats);
260
+ }
261
+
262
+ /**
263
+ * Applies the format specifiers in the first argument (if it is a string).
264
+ * The format string is split into pieces of text and values so that values
265
+ * inserted with %o (for example) can still be expanded.
266
+ * @param {any[]} args
267
+ * @returns {{values: any[], formats: {depth?: number, style?: string, isJoined?: boolean}[]}}
268
+ * The values to show and how to show each one: `depth` is used to
269
+ * summarize the value, `style` is CSS from %c and `isJoined` means that no
270
+ * space separates the value from the one before it.
271
+ */
272
+ function formatArgs(args) {
273
+ const [format, ...rest] = args;
274
+ if ('string' !== typeof format || !format.includes('%')) return {values: args, formats: []};
275
+
276
+ const values = [];
277
+ const formats = [];
278
+ let text = '';
279
+ let style = '';
280
+ const addValue = (value, depth) => {
281
+ if (text) {
282
+ values.push(text);
283
+ formats.push({style, isJoined: values.length > 1});
284
+ text = '';
285
+ }
286
+ if (depth != null) {
287
+ values.push(value);
288
+ formats.push({depth, style, isJoined: values.length > 1});
289
+ }
290
+ };
291
+ const pattern = /%([sdifoOc%])/g;
292
+ let lastIndex = 0;
293
+ for (let match; (match = pattern.exec(format));) {
294
+ const specifier = match[1];
295
+ text += format.slice(lastIndex, match.index);
296
+ lastIndex = pattern.lastIndex;
297
+ if (specifier === '%') {
298
+ text += '%';
299
+ continue;
300
+ }
301
+ // Like the browser, a specifier without a value is left as is.
302
+ if (!rest.length) {
303
+ text += match[0];
304
+ continue;
305
+ }
306
+ const value = rest.shift();
307
+ if (specifier === 'c') {
308
+ addValue();
309
+ style = `${value}`;
310
+ }
311
+ else if (specifier === 'd' || specifier === 'i') {
312
+ text += 'symbol' === typeof value ? 'NaN' : parseInt(value, 10);
313
+ }
314
+ else if (specifier === 'f') {
315
+ text += 'symbol' === typeof value ? 'NaN' : parseFloat(value);
316
+ }
317
+ else if (specifier === 's' && (value === null || ('object' !== typeof value && 'function' !== typeof value))) {
318
+ text += 'bigint' === typeof value ? `${value}n` : String(value);
319
+ }
320
+ else {
321
+ // Objects (and anything for %o or %O) are shown as values that can be
322
+ // expanded. Like the browser, %o shows strings in quotes.
323
+ addValue(value, specifier === 'o' && 'string' === typeof value ? 1 : 0);
324
+ }
325
+ }
326
+ text += format.slice(lastIndex);
327
+ addValue();
328
+
329
+ // The rest of the arguments are shown as usual.
330
+ return {values: [...values, ...rest], formats};
244
331
  }
245
332
 
246
333
  /**
@@ -253,11 +340,13 @@
253
340
  * @param {number=} depth
254
341
  * Optional, defaults to `0`. The depth used to summarize the args (see
255
342
  * summarize()).
343
+ * @param {ReturnType<formatArgs>['formats']=} formats
344
+ * Optional. How to show each arg (see formatArgs()).
256
345
  */
257
- function addLog(key, args, extra, depth = 0) {
346
+ function addLog(key, args, extra, depth = 0, formats = []) {
258
347
  // Keep track of the args so that they can be expanded later.
259
348
  const logId = `${++logCount}`;
260
- logArgsById[logId] = args.map(value => ({...summarize(value, depth), value}));
349
+ logArgsById[logId] = args.map((value, index) => ({...summarize(value, formats[index]?.depth ?? depth), value}));
261
350
 
262
351
  send({
263
352
  target: 'viewer',
@@ -265,7 +354,10 @@
265
354
  args: [{
266
355
  logId,
267
356
  key,
268
- descriptions: logArgsById[logId].map(without(['value'])),
357
+ descriptions: logArgsById[logId].map((arg, index) => {
358
+ const {style, isJoined} = formats[index] ?? {};
359
+ return {...without(['value'], arg), ...(style ? {style} : {}), ...(isJoined ? {isJoined} : {})};
360
+ }),
269
361
  groupIds: groupIds.slice(),
270
362
  ...extra,
271
363
  }]
@@ -1332,6 +1424,8 @@
1332
1424
  // The number of groups of code sent to the runner that haven't
1333
1425
  // finished running yet.
1334
1426
  runningCount: 0,
1427
+ // Set while running all of the remaining blocks (one at a time).
1428
+ isRunningAll: false,
1335
1429
  /** @type {{title: string, message: string, confirmText: string, resolve: (isConfirmed: boolean) => void}?} */
1336
1430
  dialog: null,
1337
1431
  isDisplaysScrolledToBottom: true,
@@ -1341,6 +1435,12 @@
1341
1435
  prefersDark: darkSchemeQuery.matches,
1342
1436
  runCount: 0,
1343
1437
  jsCode: visibleCode,
1438
+ wordWrap: dataset.wordWrap === 'true',
1439
+ rulers: parseRulers(dataset.rulers),
1440
+ // Unless data-hide-empty-output="false" is given, only the editor is
1441
+ // shown until there is something in the output.
1442
+ hideEmptyOutput: dataset.hideEmptyOutput !== 'false',
1443
+ hasShownOutput: false,
1344
1444
  // If the orientation wasn't specified it follows the width of the
1345
1445
  // console until the user chooses one.
1346
1446
  isDividerOrientAuto: !/^(horizontal|vertical)$/.test(dataset.dividerOrient),
@@ -1484,7 +1584,7 @@
1484
1584
  /**
1485
1585
  * The data attributes for the copied console. data-libraries-url is
1486
1586
  * left out because it usually points to files on this site while the
1487
- * copied console loads JS Box from a CDN.
1587
+ * copied console loads YourJS Box from a CDN.
1488
1588
  */
1489
1589
  exportDataset() {
1490
1590
  const {librariesUrl, ...exportDataset} = dataset;
@@ -1509,7 +1609,7 @@
1509
1609
  },
1510
1610
  {
1511
1611
  iconName: 'clear',
1512
- title: 'Clear console',
1612
+ title: 'Clear console (Ctrl+L)',
1513
1613
  callback() { this.clearConsole(); },
1514
1614
  },
1515
1615
  {
@@ -1539,7 +1639,8 @@
1539
1639
  iconName: this.runningCount ? 'spinner' : 'play',
1540
1640
  label: 'Run',
1541
1641
  className: 'primary',
1542
- title: `Run the next block of code (${isMac ? '\u2318' : 'Ctrl+'}Enter)`,
1642
+ title: `Run the next block of code (${isMac ? '\u2318' : 'Ctrl+'}Enter)`
1643
+ + ` or all of the blocks (${isMac ? '\u21e7\u2318' : 'Ctrl+Shift+'}Enter)`,
1543
1644
  callback() { this.runCode(); },
1544
1645
  disableIf() { return !this.canRunCode; }
1545
1646
  },
@@ -1568,10 +1669,14 @@
1568
1669
  canRunCode() {
1569
1670
  return this.jsCode.trim();
1570
1671
  },
1672
+ isOutputHidden() {
1673
+ return this.hideEmptyOutput && !this.hasShownOutput && !this.displays.length;
1674
+ },
1571
1675
  mainElemClassNames() {
1572
1676
  return [
1573
1677
  this.dividerOrient === 'vertical' ? 'col-orient' : 'row-orient',
1574
- this.isMovingDivider ? 'is-moving-divider' : ''
1678
+ this.isMovingDivider ? 'is-moving-divider' : '',
1679
+ this.isOutputHidden ? 'is-output-hidden' : '',
1575
1680
  ].join(' ');
1576
1681
  },
1577
1682
  mainElemStyles() {
@@ -1588,7 +1693,29 @@
1588
1693
  */
1589
1694
  onEditorKeyCombo(evt) {
1590
1695
  if ((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter' && this.canRunCode) {
1591
- this.runCode();
1696
+ if (evt.shiftKey) this.runAll();
1697
+ else this.runCode();
1698
+ }
1699
+ },
1700
+ /**
1701
+ * Runs the remaining blocks one after another. Each block runs once
1702
+ * the one before it (and any hidden blocks after it) finished so that
1703
+ * the output stays in order (see onGroupRan()).
1704
+ */
1705
+ runAll() {
1706
+ if (!this.canRunCode) return;
1707
+ this.isRunningAll = true;
1708
+ if (!this.runningCount) this.runCode();
1709
+ },
1710
+ /**
1711
+ * Called once the runner finished running a group of code.
1712
+ */
1713
+ onGroupRan() {
1714
+ this.runningCount = Math.max(0, this.runningCount - 1);
1715
+ this.runHiddenGroups();
1716
+ if (this.isRunningAll && !this.runningCount) {
1717
+ if (this.canRunCode) this.runCode();
1718
+ else this.isRunningAll = false;
1592
1719
  }
1593
1720
  },
1594
1721
  runCode() {
@@ -1788,6 +1915,8 @@
1788
1915
  this.hiddenGroups = hiddenGroups.map(group => ({...group}));
1789
1916
  this.runCount = 0;
1790
1917
  this.runningCount = 0;
1918
+ this.isRunningAll = false;
1919
+ this.hasShownOutput = false;
1791
1920
  this.runHistory = [];
1792
1921
  this.stopNavigatingHistory();
1793
1922
  messageParent({target: 'runner', func: 'reset', args: []});
@@ -1832,7 +1961,7 @@
1832
1961
  async saveFile() {
1833
1962
  this.closeMenu();
1834
1963
  const code = this.currentCode + '\n';
1835
- const suggestedName = this.fileName || (this.isTypeScript ? 'js-box.ts' : 'js-box.js');
1964
+ const suggestedName = this.fileName || (this.isTypeScript ? 'yourjs-box-code.ts' : 'yourjs-box-code.js');
1836
1965
  const fileType = this.isTypeScript
1837
1966
  ? {description: 'TypeScript', accept: {'text/typescript': ['.ts', '.mts']}}
1838
1967
  : {description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}};
@@ -1929,7 +2058,7 @@
1929
2058
  isFullPage: true,
1930
2059
  });
1931
2060
  const url = URL.createObjectURL(new Blob([html], {type: 'text/html'}));
1932
- Object.assign(document.createElement('a'), {href: url, download: 'js-box.html'}).click();
2061
+ Object.assign(document.createElement('a'), {href: url, download: 'yourjs-box.html'}).click();
1933
2062
  setTimeout(() => URL.revokeObjectURL(url), 1000);
1934
2063
  },
1935
2064
  /**
@@ -1968,6 +2097,7 @@
1968
2097
  hiddenGroups: this.hiddenGroups,
1969
2098
  runCount: this.runCount,
1970
2099
  runningCount: this.runningCount,
2100
+ hasShownOutput: this.hasShownOutput,
1971
2101
  dividerOrient: this.dividerOrient,
1972
2102
  isDividerOrientAuto: this.isDividerOrientAuto,
1973
2103
  dividerPct: this.dividerPct,
@@ -2110,6 +2240,14 @@
2110
2240
  this.toggleHistory();
2111
2241
  return;
2112
2242
  }
2243
+ // Like the browser's console, Ctrl+L clears the console (even on a
2244
+ // Mac).
2245
+ if (evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.shiftKey && evt.code === 'KeyL') {
2246
+ evt.preventDefault();
2247
+ evt.stopPropagation();
2248
+ this.clearConsole();
2249
+ return;
2250
+ }
2113
2251
  if (evt.key === 'Escape' && this.valueMenu) {
2114
2252
  this.closeValueMenu();
2115
2253
  return;
@@ -2356,9 +2494,13 @@
2356
2494
  },
2357
2495
  immediate: true,
2358
2496
  },
2359
- // Like the browser's console, keep showing the newest output unless the
2360
- // user has scrolled up to look at older output.
2361
- 'displays.length'() {
2497
+ 'displays.length'(length) {
2498
+ // Once something is in the output it stays shown (even after Clear)
2499
+ // until the console is reset.
2500
+ if (length) this.hasShownOutput = true;
2501
+
2502
+ // Like the browser's console, keep showing the newest output unless
2503
+ // the user has scrolled up to look at older output.
2362
2504
  if (this.isDisplaysScrolledToBottom) {
2363
2505
  this.$nextTick(() => {
2364
2506
  const scroller = this.$refs.displaysScroller;
@@ -2416,8 +2558,11 @@
2416
2558
  'language',
2417
2559
  'modelValue',
2418
2560
  'readOnly',
2561
+ // The columns (eg. [80, 120]) where lines are shown in the editor.
2562
+ 'rulers',
2419
2563
  'theme',
2420
2564
  'width',
2565
+ 'wordWrap',
2421
2566
  ],
2422
2567
  computed: {
2423
2568
  infoNotes() {
@@ -2461,6 +2606,12 @@
2461
2606
  fontSize(newValue) {
2462
2607
  if (newValue) this.editor.setFontSize(newValue);
2463
2608
  },
2609
+ wordWrap(newValue) {
2610
+ this.editor.session.setUseWrapMode(!!newValue);
2611
+ },
2612
+ rulers() {
2613
+ this.updateRulers();
2614
+ },
2464
2615
  modelValue(newValue) {
2465
2616
  if (newValue !== this.editor.getValue()) {
2466
2617
  // -1 puts the cursor at the start instead of selecting everything.
@@ -2480,6 +2631,18 @@
2480
2631
  // Read-only editors don't need syntax checking (eg. hints in the gutter).
2481
2632
  if (this.fontSize) editor.setFontSize(this.fontSize);
2482
2633
 
2634
+ editor.session.setUseWrapMode(!!this.wordWrap);
2635
+
2636
+ // Ace only has one ruler (its print margin) so when rulers are given
2637
+ // they are drawn instead, using the print margin's class so that each
2638
+ // theme colors them.
2639
+ if (this.rulers) {
2640
+ editor.setShowPrintMargin(false);
2641
+ this.rulersLayer = Object.assign(document.createElement('div'), {className: 'ace_layer ace_print-margin-layer'});
2642
+ editor.renderer.content.prepend(this.rulersLayer);
2643
+ editor.renderer.on('afterRender', () => this.updateRulers());
2644
+ }
2645
+
2483
2646
  if (this.readOnly) {
2484
2647
  editor.setReadOnly(true);
2485
2648
  editor.setHighlightActiveLine(false);
@@ -2558,6 +2721,24 @@
2558
2721
  if (isCombo) this.$emit('keyCombo', evt);
2559
2722
  });
2560
2723
  },
2724
+ methods: {
2725
+ /**
2726
+ * Puts a line at each ruler's column (which depends on the font size).
2727
+ */
2728
+ updateRulers() {
2729
+ const {rulersLayer, editor} = this;
2730
+ if (!rulersLayer) return;
2731
+ const {characterWidth, $padding: padding} = editor.renderer;
2732
+ const columns = Array.from(this.rulers ?? []);
2733
+ while (rulersLayer.children.length > columns.length) rulersLayer.lastChild.remove();
2734
+ while (rulersLayer.children.length < columns.length) {
2735
+ rulersLayer.append(Object.assign(document.createElement('div'), {className: 'ace_print-margin'}));
2736
+ }
2737
+ columns.forEach((column, index) => {
2738
+ rulersLayer.children[index].style.left = `${Math.round(characterWidth * column + padding)}px`;
2739
+ });
2740
+ },
2741
+ },
2561
2742
  template: '<div ref="editor" :style="style"></div>',
2562
2743
  };
2563
2744
  }
@@ -2638,6 +2819,7 @@
2638
2819
  horizontalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.5 2h12a.5.5 0 0 1 .5.5V7H1V2.5a.5.5 0 0 1 .5-.5M1 8v4.5a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V8zM0 2.5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
2639
2820
  // https://icon-sets.iconify.design/radix-icons/view-vertical/
2640
2821
  verticalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M8 2h5.5a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-.5.5H8zM7 2H1.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5H7zm-7 .5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
2822
+ runAll: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M1 2.5v11L7.5 8zM8 2.5v11L14.5 8z"/></svg>',
2641
2823
  // https://icon-sets.iconify.design/vaadin/play/
2642
2824
  play: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M2 1v14l12-7z"/></svg>',
2643
2825
  // https://icon-sets.iconify.design/mdi/error-outline/
@@ -3116,7 +3298,7 @@
3116
3298
  ' <head>',
3117
3299
  ' <meta charset="utf-8">',
3118
3300
  ' <meta name="viewport" content="width=device-width, initial-scale=1">',
3119
- ' <title>JS Box</title>',
3301
+ ' <title>YourJS Box</title>',
3120
3302
  ' <style>',
3121
3303
  ' html, body { height: 100%; margin: 0; }',
3122
3304
  ' </style>',
@@ -3226,11 +3408,16 @@
3226
3408
  */
3227
3409
  function appendLog({logId, key, descriptions, table, groupIds, groupId, isCollapsed, stack}) {
3228
3410
  key = `${key}`;
3229
- mountedApp.displays.push({
3411
+ const display = {
3230
3412
  type: 'log',
3231
3413
  key,
3232
3414
  name: key === 'result' ? 'Result' : `console.${key}`,
3233
- descriptions: Array.from(descriptions, sanitizeDescription),
3415
+ descriptions: Array.from(descriptions, description => ({
3416
+ ...sanitizeDescription(description),
3417
+ // From format specifiers (see formatArgs() in the runner).
3418
+ style: sanitizeLogStyle(Object(description).style),
3419
+ isJoined: !!Object(description).isJoined,
3420
+ })),
3234
3421
  table: table ? {
3235
3422
  headers: Array.from(table.headers, h => `${h}`),
3236
3423
  rows: Array.from(table.rows, row => ({
@@ -3245,7 +3432,43 @@
3245
3432
  isCollapsed: !!isCollapsed,
3246
3433
  stack: stack != null ? `${stack}` : null,
3247
3434
  logId: `${logId}`,
3248
- });
3435
+ // How many times in a row this message was logged.
3436
+ count: 1,
3437
+ };
3438
+
3439
+ // Like the browser, a message that is the same as the one before it is
3440
+ // shown once with a count. Only messages made up of primitives are
3441
+ // compared because objects may have changed in between.
3442
+ const lastDisplay = mountedApp.displays[mountedApp.displays.length - 1];
3443
+ const getRepeatKey = d => !d.table && !d.groupId && !d.stack && d.descriptions.every(desc => desc.isPrimitive)
3444
+ ? JSON.stringify([d.key, d.groupIds, d.descriptions])
3445
+ : null;
3446
+ const repeatKey = getRepeatKey(display);
3447
+ if (repeatKey && lastDisplay?.type === 'log' && getRepeatKey(lastDisplay) === repeatKey) {
3448
+ lastDisplay.count = (lastDisplay.count || 1) + 1;
3449
+ }
3450
+ else {
3451
+ mountedApp.displays.push(display);
3452
+ }
3453
+ }
3454
+
3455
+ /**
3456
+ * Only allows the CSS properties (from %c) that the browser's console allows
3457
+ * and never anything that loads a URL.
3458
+ * @param {*} style
3459
+ * @returns {string}
3460
+ */
3461
+ function sanitizeLogStyle(style) {
3462
+ if ('string' !== typeof style || !style.trim()) return '';
3463
+ const elem = document.createElement('span');
3464
+ elem.style.cssText = style;
3465
+ const allowed = /^(-webkit-)?(background|border|box-decoration-break|box-shadow|clear|color|cursor|display|float|font|letter-spacing|line-height|margin|outline|padding|text|white-space|word|writing-mode)(-|$)/;
3466
+ return Array.from(elem.style)
3467
+ .filter(name => allowed.test(name))
3468
+ .map(name => [name, elem.style.getPropertyValue(name)])
3469
+ .filter(([, value]) => !/\b(url|image|image-set|cross-fade|element)\s*\(/i.test(value))
3470
+ .map(([name, value]) => `${name}: ${value}`)
3471
+ .join('; ');
3249
3472
  }
3250
3473
 
3251
3474
  /**
@@ -3256,11 +3479,18 @@
3256
3479
  * @param {number=} options.column
3257
3480
  */
3258
3481
  function appendError({message, line, column}) {
3482
+ message = `${message}`;
3483
+ const lastDisplay = mountedApp.displays[mountedApp.displays.length - 1];
3484
+ if (lastDisplay?.type === 'error' && lastDisplay.message === message) {
3485
+ lastDisplay.count = (lastDisplay.count || 1) + 1;
3486
+ return;
3487
+ }
3259
3488
  mountedApp.displays.push({
3260
3489
  type: 'error',
3261
- message: `${message}`,
3490
+ message,
3262
3491
  line: +line,
3263
3492
  column: +column,
3493
+ count: 1,
3264
3494
  });
3265
3495
  }
3266
3496
 
@@ -3330,8 +3560,7 @@
3330
3560
  * NOTE: Called via main by the runner once it has run a group of code.
3331
3561
  */
3332
3562
  function onCodeRan() {
3333
- mountedApp.runningCount = Math.max(0, mountedApp.runningCount - 1);
3334
- mountedApp.runHiddenGroups();
3563
+ mountedApp.onGroupRan();
3335
3564
  }
3336
3565
 
3337
3566
  /**
@@ -3371,6 +3600,20 @@
3371
3600
  mountedApp.displays = [{type: 'notice', message: 'Console was cleared'}];
3372
3601
  }
3373
3602
 
3603
+ /**
3604
+ * Turns data-rulers (eg. "80, 120") into the columns for the rulers. Like
3605
+ * Ace's print margin, there is a ruler at column 80 by default and "" (or
3606
+ * "none") turns them off.
3607
+ * @param {string=} rulers
3608
+ * @returns {number[]}
3609
+ */
3610
+ function parseRulers(rulers) {
3611
+ return `${rulers ?? '80'}`
3612
+ .split(/[\s,]+/)
3613
+ .map(Number)
3614
+ .filter(column => Number.isInteger(column) && column > 0);
3615
+ }
3616
+
3374
3617
  /**
3375
3618
  * Only allows non-negative integers to be used as an index.
3376
3619
  * @param {*} value
@@ -3386,7 +3629,7 @@
3386
3629
  // load other files (eg. language modes) from next to these files.
3387
3630
  jsUrls: [
3388
3631
  libraryUrl('vue', 'dist/vue.global.prod.js'),
3389
- libraryUrl('ace-builds', 'src-noconflict/ace.js'),
3632
+ libraryUrl('ace-builds', 'src-min-noconflict/ace.js'),
3390
3633
  libraryUrl('prismjs', 'components/prism-core.min.js'),
3391
3634
  libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
3392
3635
  libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
@@ -3396,12 +3639,14 @@
3396
3639
  // Removes the types from TypeScript code before it is run.
3397
3640
  ...language === 'typescript' ? [libraryUrl('@babel/standalone', 'babel.min.js')] : [],
3398
3641
  ],
3399
- cssUrls: [
3400
- 'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
3642
+ cssUrls: ['data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS)],
3643
+ // These come from the CDN so they are loaded after the loading screen
3644
+ // (instead of in the head) so that it can be shown while they load.
3645
+ bodyCssUrls: [
3401
3646
  libraryUrl('prism-themes', 'themes/prism-vsc-dark-plus.min.css'),
3402
3647
  libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.css'),
3403
3648
  ],
3404
- head: targetWindow ? '<meta charset="utf-8"><title>JS Box</title>' : '',
3649
+ head: targetWindow ? '<meta charset="utf-8"><title>YourJS Box</title>' : '',
3405
3650
  htmlAttributes: 'data-theme="' + theme + '"',
3406
3651
  targetWindow,
3407
3652
  onMessage(message) {
@@ -3522,7 +3767,7 @@
3522
3767
  * The options of YourJSBox.create() which are the same as the data
3523
3768
  * attributes of a script tag.
3524
3769
  */
3525
- const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'language', 'librariesUrl', 'loading', 'runner', 'showResults', 'theme'];
3770
+ const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hideEmptyOutput', 'hidePrefix', 'importsUrl', 'language', 'librariesUrl', 'loading', 'rulers', 'runner', 'showResults', 'theme', 'wordWrap'];
3526
3771
 
3527
3772
  /**
3528
3773
  * The JavaScript API for creating consoles (available as window.YourJSBox).
@@ -3556,6 +3801,9 @@
3556
3801
  * @param {string=} options.librariesUrl
3557
3802
  * @param {string=} options.importsUrl
3558
3803
  * @param {string=} options.loading
3804
+ * @param {(string|number|number[])=} options.rulers
3805
+ * @param {(boolean|string)=} options.wordWrap
3806
+ * @param {(boolean|string)=} options.hideEmptyOutput
3559
3807
  * The same as the data attributes of a script tag.
3560
3808
  * @returns {{element: HTMLIFrameElement, destroy: () => void}}
3561
3809
  */
@@ -3586,6 +3834,75 @@
3586
3834
  insert: element => PLACEMENTS[placement](targetElement, element),
3587
3835
  });
3588
3836
  },
3837
+
3838
+ /**
3839
+ * Creates a console from each of the elements (eg. code blocks) using the
3840
+ * element's text as the code that the console starts with. A `<code>`
3841
+ * that is the only thing in a `<pre>` (eg. a Markdown code block) is
3842
+ * treated as the `<pre>`.
3843
+ * @param {string|Element|ArrayLike<Element>|Iterable<Element>} elements
3844
+ * A CSS selector for the elements, an element or a list of elements (eg.
3845
+ * an array or a NodeList).
3846
+ * @param {Object=} options
3847
+ * Optional. The same options as create() (except `target` and `code`)
3848
+ * for every console. `placement` defaults to `"replace"` and `height`
3849
+ * defaults to a height that fits the code (from 250px to 600px). The
3850
+ * `data-*` attributes of each element (eg. `data-theme="dark"`) override
3851
+ * these options, and a `language-ts` or `language-typescript` class
3852
+ * turns on TypeScript.
3853
+ * @returns {ReturnType<YourJSBox['create']>[]}
3854
+ * The consoles in the same order as the elements. Destroying a console
3855
+ * that replaced its element puts the element back.
3856
+ */
3857
+ from(elements, options) {
3858
+ options = Object(options);
3859
+ const list = 'string' === typeof elements
3860
+ ? document.querySelectorAll(elements)
3861
+ : elements?.nodeType === 1 ? [elements] : elements;
3862
+ if (list == null || 'string' === typeof list || !(Symbol.iterator in Object(list) || 'number' === typeof list.length)) {
3863
+ throw new TypeError('YourJSBox.from(): elements must be a CSS selector, an element or a list of elements.');
3864
+ }
3865
+
3866
+ return Array.from(list, source => {
3867
+ if (source?.nodeType !== 1) {
3868
+ throw new TypeError('YourJSBox.from(): every item in the list must be an element.');
3869
+ }
3870
+ const pre = source.parentElement;
3871
+ const target = source.localName === 'code' && pre?.localName === 'pre' && pre.children.length === 1
3872
+ ? pre
3873
+ : source;
3874
+ const code = source.textContent;
3875
+
3876
+ // The options for every console, then what the element says.
3877
+ const elementOptions = {};
3878
+ if (/(^|\s)lang(uage)?-(ts|typescript)(\s|$)/i.test(`${target.className} ${source.className}`)) {
3879
+ elementOptions.language = 'typescript';
3880
+ }
3881
+ for (const name of [...CONSOLE_OPTION_NAMES, 'placement', 'height']) {
3882
+ const value = source.dataset[name] ?? target.dataset[name];
3883
+ // Like the option, a height that is just a number is in pixels.
3884
+ if (value != null) elementOptions[name] = name === 'height' && /^\d+(\.\d+)?$/.test(value) ? +value : value;
3885
+ }
3886
+ const lineCount = code.trim().split('\n').length;
3887
+ const box = YourJSBox.create({
3888
+ height: Math.min(600, Math.max(250, 200 + lineCount * 18)),
3889
+ placement: 'replace',
3890
+ ...options,
3891
+ ...elementOptions,
3892
+ target,
3893
+ code,
3894
+ });
3895
+
3896
+ if ((elementOptions.placement ?? options.placement ?? 'replace') !== 'replace') return box;
3897
+ return {
3898
+ element: box.element,
3899
+ destroy() {
3900
+ if (box.element.isConnected) box.element.replaceWith(target);
3901
+ box.destroy();
3902
+ },
3903
+ };
3904
+ });
3905
+ },
3589
3906
  });
3590
3907
 
3591
3908
  // NOTE: This solution was intentionally written without using newer JS
@@ -3631,6 +3948,9 @@
3631
3948
  * parent level.
3632
3949
  * @param {string[]=} options.jsUrls
3633
3950
  * @param {string[]=} options.cssUrls
3951
+ * @param {string[]=} options.bodyCssUrls
3952
+ * Stylesheets that are loaded after `options.body` (so that it can be
3953
+ * shown while they load).
3634
3954
  * @param {string=} options.head
3635
3955
  * @param {string=} options.body
3636
3956
  * @param {(CSSStyleDeclaration|string)=} options.style
@@ -3707,6 +4027,9 @@
3707
4027
  '</head>',
3708
4028
  '<body>',
3709
4029
  options.body || '',
4030
+ (options.bodyCssUrls || []).map(function(cssUrl) {
4031
+ return '<link href="' + cssUrl + '" rel="stylesheet">';
4032
+ }).join('\n'),
3710
4033
  // The scripts are loaded after the body so that the body can be shown
3711
4034
  // while they load.
3712
4035
  (options.jsUrls || []).map(function(jsUrl) {