yourjs-box 1.2.0 → 1.3.1

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.2.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.3.1 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
2
2
  (() => {
3
3
  /**
4
4
  * Viewer IFRAME's CSS code
5
5
  * @type {string}
6
6
  */
7
- const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:12px;line-height:16px}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:12px!important;line-height:16px!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
7
+ const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
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.2.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.3.1","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
13
13
  /**
14
14
  * Viewer IFRAME's HTML code
15
15
  * @type {string}
16
16
  */
17
- const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
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.
@@ -70,6 +70,8 @@
70
70
  const RELAYABLE_FUNCS = {
71
71
  viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
72
72
  runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
73
+ // Things that the viewer (but never the runner) can ask this script to do.
74
+ host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
73
75
  };
74
76
 
75
77
  /**
@@ -893,9 +895,17 @@
893
895
  const showResults = dataset.showResults !== 'false';
894
896
  const libraryUrl = getLibraryFileUrl.bind(null, dataset.librariesUrl || DEFAULT_LIBRARIES_URL);
895
897
 
898
+ // The viewer in the page and, when the console is popped out, the viewer
899
+ // in the pop-out window. Messages from the runner go to the active one.
896
900
  /** @type {ReturnType<createCallableFrame>} */
897
- let callableViewerFrame;
898
- const sendToViewer = data => relayMessage(data, {viewer: callableViewerFrame});
901
+ let inlineViewer;
902
+ /** @type {ReturnType<createCallableFrame>?} */
903
+ let popOutViewer = null;
904
+ /** @type {Window?} */
905
+ let popOutWindow = null;
906
+ let popOutWatcher;
907
+ let activeViewer;
908
+ const sendToViewer = data => relayMessage(data, {viewer: activeViewer});
899
909
  const runner = runnerMode === 'window'
900
910
  ? createWindowRunner(sendToViewer)
901
911
  : createWorkerRunner(sendToViewer);
@@ -905,1253 +915,1607 @@
905
915
  ? dataset.theme
906
916
  : matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
907
917
 
908
- callableViewerFrame = createCallableFrame({
909
- jsCode() {
910
- let mountedApp;
911
-
912
- /**
913
- * Indicates if any of the libraries that the viewer needs failed to load (eg.
914
- * because the CDN is blocked or the user is offline).
915
- */
916
- const IS_MISSING_LIBRARIES = !window.Vue || !window.ace || !window.Prism;
917
- if (IS_MISSING_LIBRARIES) {
918
- document.querySelector('#splash').classList.add('failed');
918
+ // What the viewer can ask this script to do (see RELAYABLE_FUNCS.host).
919
+ const hostFuncs = {
920
+ /**
921
+ * Makes the IFRAME fill the window (used if full screen isn't allowed).
922
+ * @param {boolean} isMaximized
923
+ */
924
+ setMaximized(isMaximized) {
925
+ const {style} = inlineViewer.iframe;
926
+ if (isMaximized) {
927
+ inlineViewer.normalCssText ??= style.cssText;
928
+ Object.assign(style, {position: 'fixed', inset: '0', width: '100%', height: '100%', zIndex: '2147483647'});
919
929
  }
920
-
921
- const Prism = window.Prism;
922
- delete window.Prism;
923
- Prism?.plugins.autoloader.loadLanguages('javascript');
924
-
925
- /**
926
- * The minimum amount of time (in milliseconds) that the loading screen is shown.
927
- */
928
- const MIN_LOADING_TIME = 1000;
929
-
930
- /**
931
- * If the orientation isn't specified, the editor is shown below the console
932
- * instead of beside it when the console is narrower than this (in pixels).
933
- */
934
- const NARROW_WIDTH = 600;
935
-
936
- /**
937
- * @returns {"horizontal"|"vertical"}
938
- * The orientation to use when one wasn't specified.
939
- */
940
- function getAutoDividerOrient() {
941
- return innerWidth < NARROW_WIDTH ? 'horizontal' : 'vertical';
930
+ else if (inlineViewer.normalCssText != null) {
931
+ style.cssText = inlineViewer.normalCssText;
932
+ inlineViewer.normalCssText = null;
942
933
  }
943
-
944
- /**
945
- * @param {string} jsCode
946
- * @param {{[key: string]: string}} dataset
947
- * The data attributes exactly as they were specified on the script tag.
948
- * @param {{runnerMode: "worker"|"window", packageInfo: {name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}} meta
949
- */
950
- function init(jsCode, dataset, meta) {
951
- if (IS_MISSING_LIBRARIES) return;
952
-
953
- const hidePrefix = dataset.hidePrefix ?? '';
954
- const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
955
- const originalCode = unindentMin(jsCode);
956
- const {visibleCode, hiddenGroups} = extractHiddenGroups(originalCode, hidePrefix);
957
- const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
958
- const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
959
-
960
- mountedApp = Vue
961
- .createApp({
962
- data() {
963
- return {
964
- displays: [],
965
- hidePrefix,
966
- hiddenGroups: copyHiddenGroups(),
967
- runnerMode: meta.runnerMode,
968
- blockType: meta.blockType,
969
- showResults: meta.showResults,
970
- packageInfo: meta.packageInfo,
971
- libraryVersions: meta.libraryVersions,
972
- // Every group of code that was run (in order) which is used when
973
- // copying the console as HTML. Unlike the displays this is only
974
- // cleared by a reset.
975
- runHistory: [],
976
- isAboutOpen: false,
977
- aboutTab: 'about',
978
- exportCode: 'current',
979
- exportFormat: 'snippet',
980
- copyLabel: 'Copy',
981
- // The number of groups of code sent to the runner that haven't
982
- // finished running yet.
983
- runningCount: 0,
984
- /** @type {{title: string, message: string, confirmText: string, resolve: (isConfirmed: boolean) => void}?} */
985
- dialog: null,
986
- isDisplaysScrolledToBottom: true,
987
- // Like the browser's dev tools, follow the system's color scheme
988
- // unless a theme was specified.
989
- forcedTheme: /^(light|dark)$/.test(dataset.theme) ? dataset.theme : null,
990
- prefersDark: darkSchemeQuery.matches,
991
- runCount: 0,
992
- jsCode: visibleCode,
993
- // If the orientation wasn't specified it follows the width of the
994
- // console until the user chooses one.
995
- isDividerOrientAuto: !/^(horizontal|vertical)$/.test(dataset.dividerOrient),
996
- dividerOrient: /^(horizontal|vertical)$/.test(dataset.dividerOrient)
997
- ? dataset.dividerOrient
998
- : getAutoDividerOrient(),
999
- isMovingDivider: false,
1000
- dividerPct: '50%',
1001
- dividerSize: '8px',
1002
- tempDividerPct: null,
1003
- };
1004
- },
1005
- computed: {
1006
- theme() {
1007
- return this.forcedTheme ?? (this.prefersDark ? 'dark' : 'light');
1008
- },
1009
- modKey() {
1010
- return isMac ? '\u2318' : 'Ctrl';
1011
- },
1012
- runnerDescription() {
1013
- return this.runnerMode === 'window'
1014
- ? 'This page (it can use the page\'s globals and DOM)'
1015
- : 'A Web Worker (isolated from the page, no DOM)';
1016
- },
1017
- themeDescription() {
1018
- const name = this.theme === 'dark' ? 'Dark' : 'Light';
1019
- return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
1020
- },
1021
- blockTypeDescription() {
1022
- return this.blockType === 'module'
1023
- ? 'Modules (top-level await and imports work, declarations stay in each block)'
1024
- : 'Classic scripts (top-level declarations are shared between blocks)';
1025
- },
1026
- /**
1027
- * The IDs of the groups (from console.groupCollapsed() or a group that
1028
- * the user collapsed) whose messages should be hidden.
1029
- */
1030
- collapsedGroupIds() {
1031
- return new Set(
1032
- this.displays
1033
- .filter(d => d.groupId && d.isCollapsed)
1034
- .map(d => d.groupId)
1035
- );
1036
- },
1037
- layoutDescription() {
1038
- return (this.dividerOrient === 'vertical' ? 'Editor beside the console' : 'Editor below the console')
1039
- + (this.isDividerOrientAuto ? ' (automatic)' : '');
1040
- },
1041
- exportCodeOptions() {
1042
- return [
1043
- {value: 'current', label: 'Current code', note: 'The code that already ran followed by the code in the editor.'},
1044
- {value: 'original', label: 'Original code', note: 'The code this console started with.'},
1045
- {value: 'blank', label: 'Blank', note: 'An empty console with the same settings.'},
1046
- ];
1047
- },
1048
- exportFormatOptions() {
1049
- return [
1050
- {value: 'snippet', label: 'Embed snippet'},
1051
- {value: 'page', label: 'Full page'},
1052
- ];
1053
- },
1054
- exportNote() {
1055
- return this.exportCodeOptions.find(o => o.value === this.exportCode).note;
1056
- },
1057
- /**
1058
- * The code that the copied console will start with.
1059
- */
1060
- exportJsCode() {
1061
- if (this.exportCode === 'original') return originalCode;
1062
- if (this.exportCode === 'blank') return '';
1063
-
1064
- // The groups that already ran, then the groups in the editor with
1065
- // any hidden groups that haven't run yet in their original places.
1066
- const blocks = this.runHistory.slice();
1067
- const pendingHiddenGroups = this.hiddenGroups.slice();
1068
- let visibleCount = this.runCount;
1069
- const addPendingHiddenGroups = () => {
1070
- while (pendingHiddenGroups.length && pendingHiddenGroups[0].runCount <= visibleCount) {
1071
- blocks.push(pendingHiddenGroups.shift().allLines);
1072
- }
1073
- };
1074
- addPendingHiddenGroups();
1075
- for (const group of parseJSCodeGroups(this.jsCode)) {
1076
- if (!group.allLines.trim()) continue;
1077
- blocks.push(group.allLines);
1078
- visibleCount++;
1079
- addPendingHiddenGroups();
1080
- }
1081
- blocks.push(...pendingHiddenGroups.map(group => group.allLines));
1082
- return joinCodeBlocks(blocks);
1083
- },
1084
- /**
1085
- * The data attributes for the copied console. data-libraries-url is
1086
- * left out because it usually points to files on this site while the
1087
- * copied console loads JS Box from a CDN.
1088
- */
1089
- exportDataset() {
1090
- const {librariesUrl, ...exportDataset} = dataset;
1091
- return exportDataset;
1092
- },
1093
- exportHtml() {
1094
- return buildConsoleHtml({
1095
- code: this.exportJsCode,
1096
- dataset: this.exportDataset,
1097
- packageInfo: this.packageInfo,
1098
- isFullPage: this.exportFormat === 'page',
1099
- });
1100
- },
1101
- bottomButtons() {
1102
- return [
1103
- {
1104
- iconName: 'clear',
1105
- title: 'Clear console',
1106
- callback() { this.clearConsole(); },
1107
- },
1108
- {
1109
- iconName: 'refresh',
1110
- title: this.runnerMode === 'worker'
1111
- ? 'Reset (also stops any code that is still running)'
1112
- : 'Reset',
1113
- callback() { this.resetConsole(); },
1114
- },
1115
- { isSeparator: true },
1116
- {
1117
- iconName: 'horizontalView',
1118
- title: 'Show the editor below the console',
1119
- callback() { this.setDividerOrient('horizontal'); },
1120
- showIf() { return this.dividerOrient !== 'horizontal'; }
1121
- },
1122
- {
1123
- iconName: 'verticalView',
1124
- title: 'Show the editor beside the console',
1125
- callback() { this.setDividerOrient('vertical'); },
1126
- showIf() { return this.dividerOrient !== 'vertical'; }
1127
- },
1128
- {
1129
- iconName: this.runningCount ? 'spinner' : 'play',
1130
- label: 'Run',
1131
- className: 'primary',
1132
- title: `Run the next block of code (${isMac ? '\u2318' : 'Ctrl+'}Enter)`,
1133
- callback() { this.runCode(); },
1134
- disableIf() { return !this.canRunCode; }
1135
- },
1136
- ].filter(btn => !btn.showIf || btn.showIf.call(this));
1137
- },
1138
- canRunCode() {
1139
- return this.jsCode.trim();
1140
- },
1141
- mainElemClassNames() {
1142
- return [
1143
- this.dividerOrient === 'vertical' ? 'col-orient' : 'row-orient',
1144
- this.isMovingDivider ? 'is-moving-divider' : ''
1145
- ].join(' ');
1146
- },
1147
- mainElemStyles() {
934
+ },
935
+ /**
936
+ * Opens the console in a separate window. The code still runs in this
937
+ * page.
938
+ * @param {*} state
939
+ * The state of the viewer (eg. what was logged) to show in the window.
940
+ */
941
+ popOut(state) {
942
+ if (popOutWindow) {
943
+ popOutWindow.focus();
944
+ return;
945
+ }
946
+ const rect = inlineViewer.iframe.getBoundingClientRect();
947
+ const width = Math.max(820, Math.round(rect.width));
948
+ const height = Math.max(560, Math.round(rect.height));
949
+ const newWindow = open('', '_blank', `popup,width=${width},height=${height}`);
950
+ if (!newWindow) {
951
+ inlineViewer.call('onPopOutFailed');
952
+ return;
953
+ }
954
+ popOutWindow = newWindow;
955
+ popOutViewer = createViewer(newWindow, state);
956
+ // Lets the window send its state back even while it is closing (a
957
+ // message sent then can't be traced back to the window). The name
958
+ // must match POP_IN_FUNCTION_NAME in the viewer.
959
+ newWindow.yourjsBoxPopIn = state => newWindow === popOutWindow && hostFuncs.popIn(state);
960
+ activeViewer = popOutViewer;
961
+ inlineViewer.call('setPoppedOut', true);
962
+
963
+ // If the window is closed without sending its state (which it does
964
+ // when it is closed) bring the console back as it was.
965
+ popOutWatcher = setInterval(() => {
966
+ if (newWindow.closed) setTimeout(() => newWindow === popOutWindow && hostFuncs.popIn(null), 300);
967
+ }, 500);
968
+ },
969
+ /**
970
+ * Brings the console back into the page.
971
+ * @param {*} state
972
+ * The state of the pop-out window's viewer or `null` to keep the state
973
+ * from when the console was popped out.
974
+ */
975
+ popIn(state) {
976
+ const oldWindow = popOutWindow;
977
+ if (!oldWindow) return;
978
+ clearInterval(popOutWatcher);
979
+ popOutWindow = null;
980
+ popOutViewer.dispose();
981
+ popOutViewer = null;
982
+ activeViewer = inlineViewer;
983
+ inlineViewer.call('setPoppedOut', false, state);
984
+ if (!oldWindow.closed) oldWindow.close();
985
+ },
986
+ /** Asks the pop-out window to send its state back to the page. */
987
+ requestPopIn() {
988
+ if (popOutViewer) popOutViewer.call('popIn');
989
+ },
990
+ focusPopOut() {
991
+ popOutWindow?.focus();
992
+ },
993
+ };
994
+
995
+ // The pop-out window can't work without this page so it is closed too.
996
+ addEventListener('pagehide', () => popOutWindow?.close());
997
+
998
+ /**
999
+ * @param {Window=} targetWindow
1000
+ * If given the viewer is written into this window (a pop-out window).
1001
+ * @param {*=} state
1002
+ * The state to restore in the pop-out window.
1003
+ * @returns {ReturnType<createCallableFrame>}
1004
+ */
1005
+ function createViewer(targetWindow, state) {
1006
+ return createCallableFrame({
1007
+ jsCode() {
1008
+ let mountedApp;
1009
+
1010
+ /**
1011
+ * Indicates if any of the libraries that the viewer needs failed to load (eg.
1012
+ * because the CDN is blocked or the user is offline).
1013
+ */
1014
+ const IS_MISSING_LIBRARIES = !window.Vue || !window.ace || !window.Prism;
1015
+ if (IS_MISSING_LIBRARIES) {
1016
+ document.querySelector('#splash').classList.add('failed');
1017
+ }
1018
+
1019
+ const Prism = window.Prism;
1020
+ delete window.Prism;
1021
+ Prism?.plugins.autoloader.loadLanguages('javascript');
1022
+
1023
+ /**
1024
+ * The minimum amount of time (in milliseconds) that the loading screen is shown.
1025
+ */
1026
+ const MIN_LOADING_TIME = 1000;
1027
+
1028
+ /**
1029
+ * The name of the function that the page adds to a pop-out window so that the
1030
+ * window can send its state back to the page (see bringBack()).
1031
+ */
1032
+ const POP_IN_FUNCTION_NAME = 'yourjsBoxPopIn';
1033
+
1034
+ /**
1035
+ * The text sizes (as a multiple of the normal size) that can be chosen.
1036
+ */
1037
+ const TEXT_SCALES = [0.75, 0.9, 1, 1.15, 1.3, 1.5, 1.75, 2];
1038
+
1039
+ /**
1040
+ * Where the chosen text size is remembered. It applies to every console on
1041
+ * the same site.
1042
+ */
1043
+ const TEXT_SCALE_STORAGE_KEY = 'yourjs-box.textScale';
1044
+
1045
+ /**
1046
+ * @returns {number}
1047
+ * The remembered text size or `1` if there isn't one.
1048
+ */
1049
+ function loadTextScale() {
1050
+ try {
1051
+ const textScale = +localStorage.getItem(TEXT_SCALE_STORAGE_KEY);
1052
+ return TEXT_SCALES.includes(textScale) ? textScale : 1;
1053
+ }
1054
+ catch (e) {
1055
+ return 1;
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * If the orientation isn't specified, the editor is shown below the console
1061
+ * instead of beside it when the console is narrower than this (in pixels).
1062
+ */
1063
+ const NARROW_WIDTH = 600;
1064
+
1065
+ /**
1066
+ * @returns {"horizontal"|"vertical"}
1067
+ * The orientation to use when one wasn't specified.
1068
+ */
1069
+ function getAutoDividerOrient() {
1070
+ return innerWidth < NARROW_WIDTH ? 'horizontal' : 'vertical';
1071
+ }
1072
+
1073
+ /**
1074
+ * @param {string} jsCode
1075
+ * @param {{[key: string]: string}} dataset
1076
+ * The data attributes exactly as they were specified on the script tag.
1077
+ * @param {{runnerMode: "worker"|"window", packageInfo: {name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}} meta
1078
+ */
1079
+ function init(jsCode, dataset, meta) {
1080
+ if (IS_MISSING_LIBRARIES) return;
1081
+
1082
+ const hidePrefix = dataset.hidePrefix ?? '';
1083
+ const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
1084
+ const originalCode = unindentMin(jsCode);
1085
+ const {visibleCode, hiddenGroups} = extractHiddenGroups(originalCode, hidePrefix);
1086
+ const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
1087
+ const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
1088
+
1089
+ mountedApp = Vue
1090
+ .createApp({
1091
+ data() {
1148
1092
  return {
1149
- '--divider-size': this.dividerSize,
1150
- '--editor-pct': this.dividerPct,
1151
- '--temp-editor-pct': this.tempDividerPct
1093
+ displays: [],
1094
+ hidePrefix,
1095
+ hiddenGroups: copyHiddenGroups(),
1096
+ runnerMode: meta.runnerMode,
1097
+ blockType: meta.blockType,
1098
+ showResults: meta.showResults,
1099
+ packageInfo: meta.packageInfo,
1100
+ libraryVersions: meta.libraryVersions,
1101
+ // Every group of code that was run (in order) which is used when
1102
+ // copying the console as HTML. Unlike the displays this is only
1103
+ // cleared by a reset.
1104
+ runHistory: [],
1105
+ isAboutOpen: false,
1106
+ aboutTab: 'about',
1107
+ exportCode: 'current',
1108
+ exportFormat: 'snippet',
1109
+ copyLabel: 'Copy',
1110
+ // The number of groups of code sent to the runner that haven't
1111
+ // finished running yet.
1112
+ runningCount: 0,
1113
+ /** @type {{title: string, message: string, confirmText: string, resolve: (isConfirmed: boolean) => void}?} */
1114
+ dialog: null,
1115
+ isDisplaysScrolledToBottom: true,
1116
+ // Like the browser's dev tools, follow the system's color scheme
1117
+ // unless a theme was specified.
1118
+ forcedTheme: /^(light|dark)$/.test(dataset.theme) ? dataset.theme : null,
1119
+ prefersDark: darkSchemeQuery.matches,
1120
+ runCount: 0,
1121
+ jsCode: visibleCode,
1122
+ // If the orientation wasn't specified it follows the width of the
1123
+ // console until the user chooses one.
1124
+ isDividerOrientAuto: !/^(horizontal|vertical)$/.test(dataset.dividerOrient),
1125
+ dividerOrient: /^(horizontal|vertical)$/.test(dataset.dividerOrient)
1126
+ ? dataset.dividerOrient
1127
+ : getAutoDividerOrient(),
1128
+ isMovingDivider: false,
1129
+ dividerPct: '50%',
1130
+ dividerSize: '8px',
1131
+ tempDividerPct: null,
1132
+ isMenuOpen: false,
1133
+ textScale: loadTextScale(),
1134
+ isFullscreen: false,
1135
+ // Set when full screen isn't allowed so the console fills the page.
1136
+ isMaximized: false,
1137
+ // Indicates if this is the viewer in a pop-out window.
1138
+ isPopOut: meta.isPopOut,
1139
+ // Indicates if this viewer (in the page) has been popped out.
1140
+ isPoppedOut: false,
1141
+ // When popped out, the state of the console that was in the page.
1142
+ ...(meta.popOutState ?? {}),
1152
1143
  };
1153
- }
1154
- },
1155
- methods: {
1156
- /**
1157
- * @param {KeyboardEvent} evt
1158
- */
1159
- onEditorKeyCombo(evt) {
1160
- if ((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter' && this.canRunCode) {
1161
- this.runCode();
1162
- }
1163
- },
1164
- runCode() {
1165
- // Always show the output of code that is run.
1166
- this.isDisplaysScrolledToBottom = true;
1167
-
1168
- const [jsCodeGroup0, ...otherJsCodeGroups] = parseJSCodeGroups(this.jsCode);
1169
- this.runGroup(jsCodeGroup0);
1170
-
1171
- // Remove the code that was run from the editor.
1172
- this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1173
-
1174
- this.runCount++;
1175
1144
  },
1176
- /**
1177
- * Runs the next hidden group if all of the visible code that came
1178
- * before it has already been run. This is called again once the runner
1179
- * finishes running each group so that the displays stay in order.
1180
- */
1181
- runHiddenGroups() {
1182
- const {hiddenGroups} = this;
1183
- if (hiddenGroups.length && hiddenGroups[0].runCount <= this.runCount) {
1184
- this.runGroup(hiddenGroups.shift());
1145
+ computed: {
1146
+ theme() {
1147
+ return this.forcedTheme ?? (this.prefersDark ? 'dark' : 'light');
1148
+ },
1149
+ modKey() {
1150
+ return isMac ? '\u2318' : 'Ctrl';
1151
+ },
1152
+ runnerDescription() {
1153
+ return this.runnerMode === 'window'
1154
+ ? 'This page (it can use the page\'s globals and DOM)'
1155
+ : 'A Web Worker (isolated from the page, no DOM)';
1156
+ },
1157
+ themeDescription() {
1158
+ const name = this.theme === 'dark' ? 'Dark' : 'Light';
1159
+ return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
1160
+ },
1161
+ blockTypeDescription() {
1162
+ return this.blockType === 'module'
1163
+ ? 'Modules (top-level await and imports work, declarations stay in each block)'
1164
+ : 'Classic scripts (top-level declarations are shared between blocks)';
1165
+ },
1166
+ /**
1167
+ * The IDs of the groups (from console.groupCollapsed() or a group that
1168
+ * the user collapsed) whose messages should be hidden.
1169
+ */
1170
+ collapsedGroupIds() {
1171
+ return new Set(
1172
+ this.displays
1173
+ .filter(d => d.groupId && d.isCollapsed)
1174
+ .map(d => d.groupId)
1175
+ );
1176
+ },
1177
+ layoutDescription() {
1178
+ return (this.dividerOrient === 'vertical' ? 'Editor beside the console' : 'Editor below the console')
1179
+ + (this.isDividerOrientAuto ? ' (automatic)' : '');
1180
+ },
1181
+ exportCodeOptions() {
1182
+ return [
1183
+ {value: 'current', label: 'Current code', note: 'The code that already ran followed by the code in the editor.'},
1184
+ {value: 'original', label: 'Original code', note: 'The code this console started with.'},
1185
+ {value: 'blank', label: 'Blank', note: 'An empty console with the same settings.'},
1186
+ ];
1187
+ },
1188
+ exportFormatOptions() {
1189
+ return [
1190
+ {value: 'snippet', label: 'Embed snippet'},
1191
+ {value: 'page', label: 'Full page'},
1192
+ ];
1193
+ },
1194
+ exportNote() {
1195
+ return this.exportCodeOptions.find(o => o.value === this.exportCode).note;
1196
+ },
1197
+ /**
1198
+ * The code that the copied console will start with.
1199
+ */
1200
+ exportJsCode() {
1201
+ if (this.exportCode === 'original') return originalCode;
1202
+ if (this.exportCode === 'blank') return '';
1203
+
1204
+ // The groups that already ran, then the groups in the editor with
1205
+ // any hidden groups that haven't run yet in their original places.
1206
+ const blocks = this.runHistory.slice();
1207
+ const pendingHiddenGroups = this.hiddenGroups.slice();
1208
+ let visibleCount = this.runCount;
1209
+ const addPendingHiddenGroups = () => {
1210
+ while (pendingHiddenGroups.length && pendingHiddenGroups[0].runCount <= visibleCount) {
1211
+ blocks.push(pendingHiddenGroups.shift().allLines);
1212
+ }
1213
+ };
1214
+ addPendingHiddenGroups();
1215
+ for (const group of parseJSCodeGroups(this.jsCode)) {
1216
+ if (!group.allLines.trim()) continue;
1217
+ blocks.push(group.allLines);
1218
+ visibleCount++;
1219
+ addPendingHiddenGroups();
1220
+ }
1221
+ blocks.push(...pendingHiddenGroups.map(group => group.allLines));
1222
+ return joinCodeBlocks(blocks);
1223
+ },
1224
+ /**
1225
+ * The data attributes for the copied console. data-libraries-url is
1226
+ * left out because it usually points to files on this site while the
1227
+ * copied console loads JS Box from a CDN.
1228
+ */
1229
+ exportDataset() {
1230
+ const {librariesUrl, ...exportDataset} = dataset;
1231
+ return exportDataset;
1232
+ },
1233
+ exportHtml() {
1234
+ return buildConsoleHtml({
1235
+ code: this.exportJsCode,
1236
+ dataset: this.exportDataset,
1237
+ packageInfo: this.packageInfo,
1238
+ isFullPage: this.exportFormat === 'page',
1239
+ });
1240
+ },
1241
+ bottomButtons() {
1242
+ const isFullscreen = this.isFullscreen || this.isMaximized;
1243
+ return [
1244
+ {
1245
+ iconName: 'clear',
1246
+ title: 'Clear console',
1247
+ callback() { this.clearConsole(); },
1248
+ },
1249
+ {
1250
+ iconName: 'horizontalView',
1251
+ title: 'Show the editor below the console',
1252
+ callback() { this.setDividerOrient('horizontal'); },
1253
+ showIf() { return this.dividerOrient !== 'horizontal'; }
1254
+ },
1255
+ {
1256
+ iconName: 'verticalView',
1257
+ title: 'Show the editor beside the console',
1258
+ callback() { this.setDividerOrient('vertical'); },
1259
+ showIf() { return this.dividerOrient !== 'vertical'; }
1260
+ },
1261
+ {
1262
+ iconName: isFullscreen ? 'exitFullscreen' : 'fullscreen',
1263
+ title: isFullscreen ? 'Exit full screen' : 'Full screen',
1264
+ callback() { this.toggleFullscreen(); },
1265
+ },
1266
+ {
1267
+ iconName: 'more',
1268
+ title: 'More',
1269
+ className: 'more-button',
1270
+ callback() { this.toggleMenu(); },
1271
+ },
1272
+ {
1273
+ iconName: this.runningCount ? 'spinner' : 'play',
1274
+ label: 'Run',
1275
+ className: 'primary',
1276
+ title: `Run the next block of code (${isMac ? '\u2318' : 'Ctrl+'}Enter)`,
1277
+ callback() { this.runCode(); },
1278
+ disableIf() { return !this.canRunCode; }
1279
+ },
1280
+ ].filter(btn => !btn.showIf || btn.showIf.call(this));
1281
+ },
1282
+ canShrinkText() {
1283
+ return this.textScale > TEXT_SCALES[0];
1284
+ },
1285
+ canGrowText() {
1286
+ return this.textScale < TEXT_SCALES[TEXT_SCALES.length - 1];
1287
+ },
1288
+ canRunCode() {
1289
+ return this.jsCode.trim();
1290
+ },
1291
+ mainElemClassNames() {
1292
+ return [
1293
+ this.dividerOrient === 'vertical' ? 'col-orient' : 'row-orient',
1294
+ this.isMovingDivider ? 'is-moving-divider' : ''
1295
+ ].join(' ');
1296
+ },
1297
+ mainElemStyles() {
1298
+ return {
1299
+ '--divider-size': this.dividerSize,
1300
+ '--editor-pct': this.dividerPct,
1301
+ '--temp-editor-pct': this.tempDividerPct
1302
+ };
1185
1303
  }
1186
1304
  },
1187
- /**
1188
- * Adds a group of code to the displays and then runs it.
1189
- * @param {ReturnType<parseJSCodeGroups>[number]} group
1190
- */
1191
- runGroup(group) {
1192
- const {header, isHidden} = parseGroupHeader(group.headerLines, this.hidePrefix);
1193
- this.displays.push({
1194
- type: 'prism',
1195
- header,
1196
- isHidden,
1197
- isCodeShown: !isHidden,
1198
- value: group.lines,
1199
- });
1200
- this.runningCount++;
1201
- this.runHistory.push(group.allLines);
1202
- messageParent({
1203
- target: 'runner',
1204
- func: 'runCode',
1205
- args: [group.lines, {
1206
- blockType: this.blockType,
1207
- // Like the browser's console, show the value of the last
1208
- // expression (except for hidden code).
1209
- resultRange: this.showResults && !isHidden
1210
- ? findLastExpression(group.lines, this.blockType)
1211
- : null,
1212
- }],
1213
- });
1214
- },
1215
- clearConsole() {
1216
- this.displays = [];
1217
- messageParent({target: 'runner', func: 'clearLogs', args: []});
1218
- },
1219
- async resetConsole() {
1220
- const isConfirmed = await this.confirm({
1221
- title: 'Reset the console?',
1222
- message: 'The output will be cleared and the editor will go back to the original code.'
1223
- + (this.runnerMode === 'window'
1224
- ? ' Anything the code already defined on the page will stay defined.'
1225
- : ''),
1226
- confirmText: 'Reset',
1227
- });
1228
- if (!isConfirmed) return;
1229
-
1230
- this.displays = [];
1231
- this.jsCode = visibleCode;
1232
- this.hiddenGroups = copyHiddenGroups();
1233
- this.runCount = 0;
1234
- this.runningCount = 0;
1235
- this.runHistory = [];
1236
- messageParent({target: 'runner', func: 'reset', args: []});
1237
- this.runHiddenGroups();
1238
- },
1239
- /**
1240
- * Copies code that was already run into the editor so that it can be run
1241
- * again (as is or modified), first asking before replacing any code that
1242
- * is in the editor.
1243
- * @param {string} code
1244
- */
1245
- async copyToEditor(code) {
1246
- if (this.jsCode.trim() && this.jsCode !== code) {
1305
+ methods: {
1306
+ /**
1307
+ * @param {KeyboardEvent} evt
1308
+ */
1309
+ onEditorKeyCombo(evt) {
1310
+ if ((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter' && this.canRunCode) {
1311
+ this.runCode();
1312
+ }
1313
+ },
1314
+ runCode() {
1315
+ // Always show the output of code that is run.
1316
+ this.isDisplaysScrolledToBottom = true;
1317
+
1318
+ const [jsCodeGroup0, ...otherJsCodeGroups] = parseJSCodeGroups(this.jsCode);
1319
+ this.runGroup(jsCodeGroup0);
1320
+
1321
+ // Remove the code that was run from the editor.
1322
+ this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
1323
+
1324
+ this.runCount++;
1325
+ },
1326
+ /**
1327
+ * Runs the next hidden group if all of the visible code that came
1328
+ * before it has already been run. This is called again once the runner
1329
+ * finishes running each group so that the displays stay in order.
1330
+ */
1331
+ runHiddenGroups() {
1332
+ const {hiddenGroups} = this;
1333
+ if (hiddenGroups.length && hiddenGroups[0].runCount <= this.runCount) {
1334
+ this.runGroup(hiddenGroups.shift());
1335
+ }
1336
+ },
1337
+ /**
1338
+ * Adds a group of code to the displays and then runs it.
1339
+ * @param {ReturnType<parseJSCodeGroups>[number]} group
1340
+ */
1341
+ runGroup(group) {
1342
+ const {header, isHidden} = parseGroupHeader(group.headerLines, this.hidePrefix);
1343
+ this.displays.push({
1344
+ type: 'prism',
1345
+ header,
1346
+ isHidden,
1347
+ isCodeShown: !isHidden,
1348
+ value: group.lines,
1349
+ });
1350
+ this.runningCount++;
1351
+ this.runHistory.push(group.allLines);
1352
+ messageParent({
1353
+ target: 'runner',
1354
+ func: 'runCode',
1355
+ args: [group.lines, {
1356
+ blockType: this.blockType,
1357
+ // Like the browser's console, show the value of the last
1358
+ // expression (except for hidden code).
1359
+ resultRange: this.showResults && !isHidden
1360
+ ? findLastExpression(group.lines, this.blockType)
1361
+ : null,
1362
+ }],
1363
+ });
1364
+ },
1365
+ clearConsole() {
1366
+ this.displays = [];
1367
+ messageParent({target: 'runner', func: 'clearLogs', args: []});
1368
+ },
1369
+ async resetConsole() {
1247
1370
  const isConfirmed = await this.confirm({
1248
- title: 'Replace the code in the editor?',
1249
- message: 'The code that is currently in the editor will be replaced with a copy of the code you selected.',
1250
- confirmText: 'Replace',
1371
+ title: 'Reset the console?',
1372
+ message: 'The output will be cleared and the editor will go back to the original code.'
1373
+ + (this.runnerMode === 'window'
1374
+ ? ' Anything the code already defined on the page will stay defined.'
1375
+ : ''),
1376
+ confirmText: 'Reset',
1251
1377
  });
1252
1378
  if (!isConfirmed) return;
1379
+
1380
+ this.displays = [];
1381
+ this.jsCode = visibleCode;
1382
+ this.hiddenGroups = copyHiddenGroups();
1383
+ this.runCount = 0;
1384
+ this.runningCount = 0;
1385
+ this.runHistory = [];
1386
+ messageParent({target: 'runner', func: 'reset', args: []});
1387
+ this.runHiddenGroups();
1388
+ },
1389
+ /**
1390
+ * Copies code that was already run into the editor so that it can be run
1391
+ * again (as is or modified), first asking before replacing any code that
1392
+ * is in the editor.
1393
+ * @param {string} code
1394
+ */
1395
+ async copyToEditor(code) {
1396
+ if (this.jsCode.trim() && this.jsCode !== code) {
1397
+ const isConfirmed = await this.confirm({
1398
+ title: 'Replace the code in the editor?',
1399
+ message: 'The code that is currently in the editor will be replaced with a copy of the code you selected.',
1400
+ confirmText: 'Replace',
1401
+ });
1402
+ if (!isConfirmed) return;
1403
+ }
1404
+ this.jsCode = code;
1405
+ },
1406
+ /**
1407
+ * Shows a dialog asking the user to confirm something.
1408
+ * @param {{title: string, message: string, confirmText: string}} options
1409
+ * @returns {Promise<boolean>}
1410
+ */
1411
+ confirm(options) {
1412
+ this.dialog?.resolve(false);
1413
+ return new Promise(resolve => {
1414
+ this.dialog = {...options, resolve};
1415
+ this.$nextTick(() => this.$refs.dialogConfirmButton?.focus());
1416
+ });
1417
+ },
1418
+ /**
1419
+ * @param {"about"|"html"} tab
1420
+ */
1421
+ openAbout(tab) {
1422
+ this.aboutTab = tab;
1423
+ this.isAboutOpen = true;
1424
+ this.$nextTick(() => this.$refs.aboutCloseButton?.focus());
1425
+ },
1426
+ closeAbout() {
1427
+ this.isAboutOpen = false;
1428
+ },
1429
+ async copyExport() {
1430
+ const text = this.exportHtml;
1431
+ try {
1432
+ await navigator.clipboard.writeText(text);
1433
+ }
1434
+ catch (e) {
1435
+ // Falls back to the older way of copying text.
1436
+ const textarea = Object.assign(document.createElement('textarea'), {value: text});
1437
+ document.body.appendChild(textarea);
1438
+ textarea.select();
1439
+ document.execCommand('copy');
1440
+ textarea.remove();
1441
+ }
1442
+ this.copyLabel = 'Copied!';
1443
+ clearTimeout(this.copyLabelTimeout);
1444
+ this.copyLabelTimeout = setTimeout(() => this.copyLabel = 'Copy', 1600);
1445
+ },
1446
+ /**
1447
+ * Downloads the console as a full HTML page.
1448
+ */
1449
+ downloadExport() {
1450
+ const html = buildConsoleHtml({
1451
+ code: this.exportJsCode,
1452
+ dataset: this.exportDataset,
1453
+ packageInfo: this.packageInfo,
1454
+ isFullPage: true,
1455
+ });
1456
+ const url = URL.createObjectURL(new Blob([html], {type: 'text/html'}));
1457
+ Object.assign(document.createElement('a'), {href: url, download: 'js-box.html'}).click();
1458
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1459
+ },
1460
+ /**
1461
+ * Clicking a group's header (from console.group()) collapses or expands
1462
+ * the group unless a value in the header was clicked.
1463
+ * @param {*} display
1464
+ * @param {MouseEvent} evt
1465
+ */
1466
+ onLogRowClick(display, evt) {
1467
+ if (display.groupId && !evt.target.closest('.js-value-header.expandable')) {
1468
+ display.isCollapsed = !display.isCollapsed;
1469
+ }
1470
+ },
1471
+ /**
1472
+ * Indicates if a display is inside of a collapsed group.
1473
+ */
1474
+ isInCollapsedGroup(display) {
1475
+ const {collapsedGroupIds} = this;
1476
+ return !!collapsedGroupIds.size && !!display.groupIds?.some(id => collapsedGroupIds.has(id));
1477
+ },
1478
+ /**
1479
+ * @returns {*}
1480
+ * A copy of the parts of the console's state that are moved between
1481
+ * the page and a pop-out window.
1482
+ */
1483
+ getState() {
1484
+ return JSON.parse(JSON.stringify({
1485
+ displays: this.displays,
1486
+ jsCode: this.jsCode,
1487
+ runHistory: this.runHistory,
1488
+ hiddenGroups: this.hiddenGroups,
1489
+ runCount: this.runCount,
1490
+ runningCount: this.runningCount,
1491
+ dividerOrient: this.dividerOrient,
1492
+ isDividerOrientAuto: this.isDividerOrientAuto,
1493
+ dividerPct: this.dividerPct,
1494
+ }));
1495
+ },
1496
+ /**
1497
+ * @param {ReturnType<this['getState']>} state
1498
+ */
1499
+ restoreState(state) {
1500
+ Object.assign(this, state);
1501
+ this.isDisplaysScrolledToBottom = true;
1502
+ },
1503
+ toggleMenu() {
1504
+ if (this.isMenuOpen) {
1505
+ this.closeMenu();
1506
+ }
1507
+ else {
1508
+ this.isMenuOpen = true;
1509
+ this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
1510
+ }
1511
+ },
1512
+ closeMenu(shouldFocusButton) {
1513
+ this.isMenuOpen = false;
1514
+ if (shouldFocusButton) document.querySelector('#bottomNav .more-button')?.focus();
1515
+ },
1516
+ /**
1517
+ * Lets the arrow keys move between the menu's items.
1518
+ * @param {KeyboardEvent} evt
1519
+ */
1520
+ onMenuKeyDown(evt) {
1521
+ if (evt.key === 'Escape') {
1522
+ evt.stopPropagation();
1523
+ this.closeMenu(true);
1524
+ }
1525
+ else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
1526
+ evt.preventDefault();
1527
+ const items = [...this.$refs.moreMenu.querySelectorAll('.menu-item:not(:disabled)')];
1528
+ const index = items.indexOf(document.activeElement);
1529
+ const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
1530
+ next?.focus();
1531
+ }
1532
+ },
1533
+ /**
1534
+ * @param {number} direction
1535
+ * `1` to make the text bigger or `-1` to make it smaller.
1536
+ */
1537
+ changeTextScale(direction) {
1538
+ const index = TEXT_SCALES.indexOf(this.textScale) + direction;
1539
+ if (index >= 0 && index < TEXT_SCALES.length) this.setTextScale(TEXT_SCALES[index]);
1540
+ },
1541
+ setTextScale(textScale) {
1542
+ this.textScale = textScale;
1543
+ try {
1544
+ localStorage.setItem(TEXT_SCALE_STORAGE_KEY, `${textScale}`);
1545
+ }
1546
+ catch (e) {}
1547
+ },
1548
+ /**
1549
+ * Uses the browser's full screen if it's allowed, otherwise the
1550
+ * console is made to fill the page.
1551
+ */
1552
+ async toggleFullscreen() {
1553
+ if (this.isMaximized) {
1554
+ this.setMaximized(false);
1555
+ }
1556
+ else if (document.fullscreenElement) {
1557
+ await document.exitFullscreen();
1558
+ }
1559
+ else {
1560
+ try {
1561
+ await document.documentElement.requestFullscreen();
1562
+ }
1563
+ catch (e) {
1564
+ // A pop-out window can't be made to fill anything else.
1565
+ if (!this.isPopOut) this.setMaximized(true);
1566
+ }
1567
+ }
1568
+ },
1569
+ setMaximized(isMaximized) {
1570
+ this.isMaximized = isMaximized;
1571
+ messageParent({target: 'host', func: 'setMaximized', args: [isMaximized]});
1572
+ },
1573
+ /**
1574
+ * Moves the console into a separate window. The code still runs in
1575
+ * the page.
1576
+ */
1577
+ popOut() {
1578
+ this.closeMenu();
1579
+ if (this.isMaximized) this.setMaximized(false);
1580
+ messageParent({target: 'host', func: 'popOut', args: [this.getState()]});
1581
+ },
1582
+ /**
1583
+ * Moves the console from the pop-out window back into the page.
1584
+ */
1585
+ bringBack() {
1586
+ this.hasSentState = true;
1587
+ // The page provides a function to call directly because a message
1588
+ // sent while this window is closing can't be traced back to it.
1589
+ const popIn = window[POP_IN_FUNCTION_NAME];
1590
+ if ('function' === typeof popIn) popIn(this.getState());
1591
+ else messageParent({target: 'host', func: 'popIn', args: [this.getState()]});
1592
+ },
1593
+ focusPopOut() {
1594
+ messageParent({target: 'host', func: 'focusPopOut', args: []});
1595
+ },
1596
+ requestPopIn() {
1597
+ messageParent({target: 'host', func: 'requestPopIn', args: []});
1598
+ },
1599
+ /**
1600
+ * Closes the menu when clicking outside of it.
1601
+ * @param {MouseEvent} evt
1602
+ */
1603
+ onWindowMouseDown(evt) {
1604
+ if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
1605
+ this.closeMenu();
1606
+ }
1607
+ },
1608
+ /**
1609
+ * @param {KeyboardEvent} evt
1610
+ */
1611
+ onWindowKeyDown(evt) {
1612
+ if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
1613
+ this.setMaximized(false);
1614
+ }
1615
+ },
1616
+ closeDialog(isConfirmed) {
1617
+ const {dialog} = this;
1618
+ this.dialog = null;
1619
+ dialog?.resolve(isConfirmed);
1620
+ },
1621
+ /**
1622
+ * Sets the orientation chosen by the user which stops it from
1623
+ * automatically following the width of the console.
1624
+ * @param {"horizontal"|"vertical"} orient
1625
+ */
1626
+ setDividerOrient(orient) {
1627
+ this.isDividerOrientAuto = false;
1628
+ this.dividerOrient = orient;
1629
+ },
1630
+ onWindowResize() {
1631
+ if (this.isDividerOrientAuto) {
1632
+ this.dividerOrient = getAutoDividerOrient();
1633
+ }
1634
+ },
1635
+ onDisplaysScroll() {
1636
+ const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
1637
+ this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
1638
+ },
1639
+ getEditorPct(evt) {
1640
+ const rect = this.$refs.main.getBoundingClientRect();
1641
+ const {dividerOrient} = this;
1642
+ const x = evt.pageX - rect.left;
1643
+ const y = evt.pageY - rect.top;
1644
+ const w = rect.width;
1645
+ const h = rect.height;
1646
+ const pct = 100 - 100 * Math.min(
1647
+ Math.max(
1648
+ 0.2,
1649
+ dividerOrient === 'vertical' ? x / w : (y / h)
1650
+ ),
1651
+ 0.8
1652
+ );
1653
+ return pct + '%';
1654
+ },
1655
+ onMainElemMouseDown(evt) {
1656
+ if (evt.target === this.$refs.mainDivider) {
1657
+ this.isMovingDivider = true;
1658
+ this.tempDividerPct = this.getEditorPct(evt);
1659
+ }
1660
+ },
1661
+ onWindowMouseMove(evt) {
1662
+ if (this.isMovingDivider) {
1663
+ this.tempDividerPct = this.getEditorPct(evt);
1664
+ }
1665
+ },
1666
+ onWindowMouseUp(evt) {
1667
+ if (this.isMovingDivider) {
1668
+ this.isMovingDivider = false;
1669
+ this.dividerPct = this.getEditorPct(evt);
1670
+ }
1671
+ },
1672
+ onWindowError(evt) {
1673
+ this.displays.push({
1674
+ type: 'error',
1675
+ message: evt.error?.stack ?? evt.message ?? evt.error?.message ?? `${evt.error}`,
1676
+ line: evt.lineno,
1677
+ column: evt.colno,
1678
+ })
1253
1679
  }
1254
- this.jsCode = code;
1255
- },
1256
- /**
1257
- * Shows a dialog asking the user to confirm something.
1258
- * @param {{title: string, message: string, confirmText: string}} options
1259
- * @returns {Promise<boolean>}
1260
- */
1261
- confirm(options) {
1262
- this.dialog?.resolve(false);
1263
- return new Promise(resolve => {
1264
- this.dialog = {...options, resolve};
1265
- this.$nextTick(() => this.$refs.dialogConfirmButton?.focus());
1266
- });
1267
- },
1268
- /**
1269
- * @param {"about"|"html"} tab
1270
- */
1271
- openAbout(tab) {
1272
- this.aboutTab = tab;
1273
- this.isAboutOpen = true;
1274
- this.$nextTick(() => this.$refs.aboutCloseButton?.focus());
1275
- },
1276
- closeAbout() {
1277
- this.isAboutOpen = false;
1278
- },
1279
- async copyExport() {
1280
- const text = this.exportHtml;
1281
- try {
1282
- await navigator.clipboard.writeText(text);
1283
- }
1284
- catch (e) {
1285
- // Falls back to the older way of copying text.
1286
- const textarea = Object.assign(document.createElement('textarea'), {value: text});
1287
- document.body.appendChild(textarea);
1288
- textarea.select();
1289
- document.execCommand('copy');
1290
- textarea.remove();
1291
- }
1292
- this.copyLabel = 'Copied!';
1293
- clearTimeout(this.copyLabelTimeout);
1294
- this.copyLabelTimeout = setTimeout(() => this.copyLabel = 'Copy', 1600);
1295
1680
  },
1296
- /**
1297
- * Downloads the console as a full HTML page.
1298
- */
1299
- downloadExport() {
1300
- const html = buildConsoleHtml({
1301
- code: this.exportJsCode,
1302
- dataset: this.exportDataset,
1303
- packageInfo: this.packageInfo,
1304
- isFullPage: true,
1305
- });
1306
- const url = URL.createObjectURL(new Blob([html], {type: 'text/html'}));
1307
- Object.assign(document.createElement('a'), {href: url, download: 'js-box.html'}).click();
1308
- setTimeout(() => URL.revokeObjectURL(url), 1000);
1681
+ watch: {
1682
+ // Used by the CSS to size the text.
1683
+ textScale: {
1684
+ handler(textScale) {
1685
+ document.documentElement.style.setProperty('--text-scale', textScale);
1686
+ },
1687
+ immediate: true,
1688
+ },
1689
+ theme: {
1690
+ handler(theme) {
1691
+ document.documentElement.dataset.theme = theme;
1692
+ },
1693
+ immediate: true,
1694
+ },
1695
+ // Like the browser's console, keep showing the newest output unless the
1696
+ // user has scrolled up to look at older output.
1697
+ 'displays.length'() {
1698
+ if (this.isDisplaysScrolledToBottom) {
1699
+ this.$nextTick(() => {
1700
+ const scroller = this.$refs.displaysScroller;
1701
+ scroller.scrollTop = scroller.scrollHeight;
1702
+ });
1703
+ }
1704
+ },
1309
1705
  },
1310
- /**
1311
- * Clicking a group's header (from console.group()) collapses or expands
1312
- * the group unless a value in the header was clicked.
1313
- * @param {*} display
1314
- * @param {MouseEvent} evt
1315
- */
1316
- onLogRowClick(display, evt) {
1317
- if (display.groupId && !evt.target.closest('.js-value-header.expandable')) {
1318
- display.isCollapsed = !display.isCollapsed;
1706
+ mounted() {
1707
+ addEventListener('mousemove', this.onWindowMouseMove);
1708
+ addEventListener('mousedown', this.onWindowMouseDown);
1709
+ addEventListener('keydown', this.onWindowKeyDown);
1710
+ addEventListener('resize', this.onWindowResize);
1711
+ addEventListener('mouseup', this.onWindowMouseUp);
1712
+ addEventListener('error', this.onWindowError);
1713
+ darkSchemeQuery.addEventListener('change', e => this.prefersDark = e.matches);
1714
+ document.addEventListener('fullscreenchange', () => this.isFullscreen = !!document.fullscreenElement);
1715
+
1716
+ // A pop-out window sends its state back to the page when it is closed.
1717
+ if (this.isPopOut) {
1718
+ addEventListener('pagehide', () => this.hasSentState || this.bringBack());
1319
1719
  }
1720
+
1721
+ // Hidden code that came before all visible code gets run immediately
1722
+ // (unless this is a pop-out window still waiting for code to finish).
1723
+ if (!this.runningCount) this.runHiddenGroups();
1724
+
1725
+ // Shows the loading screen for at least a moment and then fades it out
1726
+ // (but not in a pop-out window which should appear right away).
1727
+ setTimeout(
1728
+ () => document.querySelector('#splash').classList.add('hidden'),
1729
+ Math.max(0, (this.isPopOut ? 0 : MIN_LOADING_TIME) - performance.now())
1730
+ );
1731
+ }
1732
+ })
1733
+ .component('ace-editor', getAceComponentProps())
1734
+ .component('prism', getPrismComponentProps())
1735
+ .component('icon', getIconComponentProps())
1736
+ .component('js-value', getJSValueComponentProps())
1737
+ .mount(document.querySelector('#vueApp'));
1738
+ }
1739
+
1740
+ function getAceComponentProps() {
1741
+ return {
1742
+ data() {
1743
+ return {
1744
+ annotations: [],
1745
+ };
1746
+ },
1747
+ props: [
1748
+ 'fontSize',
1749
+ 'height',
1750
+ 'keybinding',
1751
+ 'language',
1752
+ 'modelValue',
1753
+ 'readOnly',
1754
+ 'theme',
1755
+ 'width',
1756
+ ],
1757
+ computed: {
1758
+ infoNotes() {
1759
+ return this.annotations.filter(({type}) => type === 'info');
1320
1760
  },
1321
- /**
1322
- * Indicates if a display is inside of a collapsed group.
1323
- */
1324
- isInCollapsedGroup(display) {
1325
- const {collapsedGroupIds} = this;
1326
- return !!collapsedGroupIds.size && !!display.groupIds?.some(id => collapsedGroupIds.has(id));
1327
- },
1328
- closeDialog(isConfirmed) {
1329
- const {dialog} = this;
1330
- this.dialog = null;
1331
- dialog?.resolve(isConfirmed);
1761
+ warningNotes() {
1762
+ return this.annotations.filter(({type}) => type === 'warning');
1332
1763
  },
1333
- /**
1334
- * Sets the orientation chosen by the user which stops it from
1335
- * automatically following the width of the console.
1336
- * @param {"horizontal"|"vertical"} orient
1337
- */
1338
- setDividerOrient(orient) {
1339
- this.isDividerOrientAuto = false;
1340
- this.dividerOrient = orient;
1764
+ errorNotes() {
1765
+ return this.annotations.filter(({type}) => type === 'error');
1341
1766
  },
1342
- onWindowResize() {
1343
- if (this.isDividerOrientAuto) {
1344
- this.dividerOrient = getAutoDividerOrient();
1345
- }
1767
+ style() {
1768
+ return {
1769
+ height: this.height != null
1770
+ ? 'number' === typeof this.height
1771
+ ? this.height + 'px'
1772
+ : this.height
1773
+ : '150px',
1774
+ width: this.width != null
1775
+ ? 'number' === typeof this.width
1776
+ ? this.width + 'px'
1777
+ : this.width
1778
+ : '100%',
1779
+ };
1346
1780
  },
1347
- onDisplaysScroll() {
1348
- const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
1349
- this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
1781
+ modeSig() {
1782
+ return `ace/mode/${this.language ?? 'text'}`;
1350
1783
  },
1351
- getEditorPct(evt) {
1352
- const rect = this.$refs.main.getBoundingClientRect();
1353
- const {dividerOrient} = this;
1354
- const x = evt.pageX - rect.left;
1355
- const y = evt.pageY - rect.top;
1356
- const w = rect.width;
1357
- const h = rect.height;
1358
- const pct = 100 - 100 * Math.min(
1359
- Math.max(
1360
- 0.2,
1361
- dividerOrient === 'vertical' ? x / w : (y / h)
1362
- ),
1363
- 0.8
1784
+ themeSig() {
1785
+ let theme = `${this.theme ?? 'light'}`.replace(
1786
+ /^light$|(^dark$)/i,
1787
+ (_, isDark) => `cloud_editor${isDark ? '_dark' : ''}`
1364
1788
  );
1365
- return pct + '%';
1366
- },
1367
- onMainElemMouseDown(evt) {
1368
- if (evt.target === this.$refs.mainDivider) {
1369
- this.isMovingDivider = true;
1370
- this.tempDividerPct = this.getEditorPct(evt);
1371
- }
1372
- },
1373
- onWindowMouseMove(evt) {
1374
- if (this.isMovingDivider) {
1375
- this.tempDividerPct = this.getEditorPct(evt);
1376
- }
1377
- },
1378
- onWindowMouseUp(evt) {
1379
- if (this.isMovingDivider) {
1380
- this.isMovingDivider = false;
1381
- this.dividerPct = this.getEditorPct(evt);
1382
- }
1383
- },
1384
- onWindowError(evt) {
1385
- this.displays.push({
1386
- type: 'error',
1387
- message: evt.error?.stack ?? evt.message ?? evt.error?.message ?? `${evt.error}`,
1388
- line: evt.lineno,
1389
- column: evt.colno,
1390
- })
1789
+ return `ace/theme/${theme}`;
1391
1790
  }
1392
1791
  },
1393
1792
  watch: {
1394
- theme: {
1395
- handler(theme) {
1396
- document.documentElement.dataset.theme = theme;
1397
- },
1398
- immediate: true,
1793
+ themeSig(newValue) {
1794
+ this.editor.setTheme(newValue);
1399
1795
  },
1400
- // Like the browser's console, keep showing the newest output unless the
1401
- // user has scrolled up to look at older output.
1402
- 'displays.length'() {
1403
- if (this.isDisplaysScrolledToBottom) {
1404
- this.$nextTick(() => {
1405
- const scroller = this.$refs.displaysScroller;
1406
- scroller.scrollTop = scroller.scrollHeight;
1407
- });
1408
- }
1796
+ fontSize(newValue) {
1797
+ if (newValue) this.editor.setFontSize(newValue);
1409
1798
  },
1799
+ modelValue(newValue) {
1800
+ if (newValue !== this.editor.getValue()) {
1801
+ // -1 puts the cursor at the start instead of selecting everything.
1802
+ this.editor.setValue(newValue, -1);
1803
+ }
1804
+ }
1410
1805
  },
1411
- mounted() {
1412
- addEventListener('mousemove', this.onWindowMouseMove);
1413
- addEventListener('resize', this.onWindowResize);
1414
- addEventListener('mouseup', this.onWindowMouseUp);
1415
- addEventListener('error', this.onWindowError);
1416
- darkSchemeQuery.addEventListener('change', e => this.prefersDark = e.matches);
1417
-
1418
- // Hidden code that came before all visible code gets run immediately.
1419
- this.runHiddenGroups();
1420
-
1421
- // Shows the loading screen for at least a moment and then fades it out.
1422
- setTimeout(
1423
- () => document.querySelector('#splash').classList.add('hidden'),
1424
- Math.max(0, MIN_LOADING_TIME - performance.now())
1425
- );
1426
- }
1427
- })
1428
- .component('ace-editor', getAceComponentProps())
1429
- .component('prism', getPrismComponentProps())
1430
- .component('icon', getIconComponentProps())
1431
- .component('js-value', getJSValueComponentProps())
1432
- .mount(document.querySelector('#vueApp'));
1433
- }
1434
-
1435
- function getAceComponentProps() {
1436
- return {
1437
- data() {
1438
- return {
1439
- annotations: [],
1440
- };
1441
- },
1442
- props: [
1443
- 'height',
1444
- 'keybinding',
1445
- 'language',
1446
- 'modelValue',
1447
- 'readOnly',
1448
- 'theme',
1449
- 'width',
1450
- ],
1451
- computed: {
1452
- infoNotes() {
1453
- return this.annotations.filter(({type}) => type === 'info');
1454
- },
1455
- warningNotes() {
1456
- return this.annotations.filter(({type}) => type === 'warning');
1457
- },
1458
- errorNotes() {
1459
- return this.annotations.filter(({type}) => type === 'error');
1806
+ async mounted() {
1807
+ // Get the ace editor set up with the language and theme.
1808
+ const editor = ace.edit(this.$refs.editor, {
1809
+ value: this.modelValue,
1810
+ mode: this.modeSig,
1811
+ theme: this.themeSig,
1812
+ });
1813
+ this.editor = editor;
1814
+
1815
+ // Read-only editors don't need syntax checking (eg. hints in the gutter).
1816
+ if (this.fontSize) editor.setFontSize(this.fontSize);
1817
+
1818
+ if (this.readOnly) {
1819
+ editor.setReadOnly(true);
1820
+ editor.setHighlightActiveLine(false);
1821
+ editor.session.setUseWorker(false);
1822
+ }
1823
+
1824
+ // Use VSCode keybinding but remove the CTRL+ENTER and CTRL+SHIFT+ENTER
1825
+ // (or on Mac CMD+ENTER and CMD+SHIFT+ENTER) commands so that they can be
1826
+ // caught by custom code.
1827
+ editor.setKeyboardHandler('ace/keyboard/vscode');
1828
+ editor.commands.removeCommand('addLineAfter');
1829
+ editor.commands.removeCommand('addLineBefore');
1830
+
1831
+ // When the value in the ace editor is updated make sure to emit the
1832
+ // modelValue update event.
1833
+ editor.on('change', () => this.$emit('update:modelValue', editor.getValue()));
1834
+
1835
+ // When the annotations change in the editor go ahead and trigger the
1836
+ // different note events if necessary.
1837
+ editor.session.on("changeAnnotation", () => {
1838
+ const oldErrorsJSON = JSON.stringify(this.errorNotes);
1839
+ const oldInfosJSON = JSON.stringify(this.infoNotes);
1840
+ const oldWarningsJSON = JSON.stringify(this.warningNotes);
1841
+ this.annotations = editor.getSession().getAnnotations();
1842
+ const {errorNotes, infoNotes, warningNotes} = this;
1843
+ if (oldErrorsJSON !== JSON.stringify(errorNotes)) {
1844
+ this.$emit('changeErrorNotes', errorNotes);
1845
+ }
1846
+ if (oldInfosJSON !== JSON.stringify(infoNotes)) {
1847
+ this.$emit('changeInfoNotes', infoNotes);
1848
+ }
1849
+ if (oldWarningsJSON !== JSON.stringify(warningNotes)) {
1850
+ this.$emit('changeWarningNotes', warningNotes);
1851
+ }
1852
+ });
1853
+
1854
+ // Attempt to capture key combos.
1855
+ editor.textInput.getElement().addEventListener('keydown', (evt) => {
1856
+ const isComboKeyCode = evt.keyCode === 16 // SHIFT
1857
+ || evt.keyCode === 17 // CTRL
1858
+ || evt.keyCode === 18 // ALT or OPT
1859
+ || evt.keyCode === 91 // META
1860
+ || evt.keyCode === 92 // META
1861
+ || evt.keyCode === 93 // META;
1862
+
1863
+ // Dont do anything if only a combo key is being pressed.
1864
+ if (isComboKeyCode) return;
1865
+
1866
+ // If this is really a key combo then go ahead and emit that event.
1867
+ const isCombo = evt.metaKey
1868
+ || evt.ctrlKey
1869
+ || evt.altKey
1870
+ || (evt.shiftKey && (evt.key ?? '').length > 1);
1871
+ if (isCombo) this.$emit('keyCombo', evt);
1872
+ });
1460
1873
  },
1461
- style() {
1874
+ template: '<div ref="editor" :style="style"></div>',
1875
+ };
1876
+ }
1877
+
1878
+ function getPrismComponentProps() {
1879
+ return {
1880
+ data() {
1462
1881
  return {
1463
- height: this.height != null
1464
- ? 'number' === typeof this.height
1465
- ? this.height + 'px'
1466
- : this.height
1467
- : '150px',
1468
- width: this.width != null
1469
- ? 'number' === typeof this.width
1470
- ? this.width + 'px'
1471
- : this.width
1472
- : '100%',
1882
+ annotations: [],
1473
1883
  };
1474
1884
  },
1475
- modeSig() {
1476
- return `ace/mode/${this.language ?? 'text'}`;
1885
+ props: ['code', 'isDark', 'language', 'lineNumbers', 'matchBraces'],
1886
+ computed: {
1887
+ html() {},
1888
+ preStyle() {
1889
+ return {
1890
+ filter: (this.isDark ?? false) !== false
1891
+ ? 'none'
1892
+ : 'invert(1) hue-rotate(180deg) brightness(1.1)',
1893
+ margin: 0,
1894
+ paddingTop: '0.5em',
1895
+ paddingBottom: '0.5em'
1896
+ };
1897
+ }
1477
1898
  },
1478
- themeSig() {
1479
- let theme = `${this.theme ?? 'light'}`.replace(
1480
- /^light$|(^dark$)/i,
1481
- (_, isDark) => `cloud_editor${isDark ? '_dark' : ''}`
1482
- );
1483
- return `ace/theme/${theme}`;
1484
- }
1485
- },
1486
- watch: {
1487
- themeSig(newValue) {
1488
- this.editor.setTheme(newValue);
1899
+ watch: {
1900
+ code() { this.redraw(); },
1901
+ language() { this.redraw(); },
1902
+ lineNumbers() { this.redraw(); },
1903
+ matchBraces() { this.redraw(); },
1489
1904
  },
1490
- modelValue(newValue) {
1491
- if (newValue !== this.editor.getValue()) {
1492
- // -1 puts the cursor at the start instead of selecting everything.
1493
- this.editor.setValue(newValue, -1);
1494
- }
1495
- }
1496
- },
1497
- async mounted() {
1498
- // Get the ace editor set up with the language and theme.
1499
- const editor = ace.edit(this.$refs.editor, {
1500
- value: this.modelValue,
1501
- mode: this.modeSig,
1502
- theme: this.themeSig,
1503
- });
1504
- this.editor = editor;
1505
-
1506
- // Read-only editors don't need syntax checking (eg. hints in the gutter).
1507
- if (this.readOnly) {
1508
- editor.setReadOnly(true);
1509
- editor.setHighlightActiveLine(false);
1510
- editor.session.setUseWorker(false);
1511
- }
1512
-
1513
- // Use VSCode keybinding but remove the CTRL+ENTER and CTRL+SHIFT+ENTER
1514
- // (or on Mac CMD+ENTER and CMD+SHIFT+ENTER) commands so that they can be
1515
- // caught by custom code.
1516
- editor.setKeyboardHandler('ace/keyboard/vscode');
1517
- editor.commands.removeCommand('addLineAfter');
1518
- editor.commands.removeCommand('addLineBefore');
1519
-
1520
- // When the value in the ace editor is updated make sure to emit the
1521
- // modelValue update event.
1522
- editor.on('change', () => this.$emit('update:modelValue', editor.getValue()));
1523
-
1524
- // When the annotations change in the editor go ahead and trigger the
1525
- // different note events if necessary.
1526
- editor.session.on("changeAnnotation", () => {
1527
- const oldErrorsJSON = JSON.stringify(this.errorNotes);
1528
- const oldInfosJSON = JSON.stringify(this.infoNotes);
1529
- const oldWarningsJSON = JSON.stringify(this.warningNotes);
1530
- this.annotations = editor.getSession().getAnnotations();
1531
- const {errorNotes, infoNotes, warningNotes} = this;
1532
- if (oldErrorsJSON !== JSON.stringify(errorNotes)) {
1533
- this.$emit('changeErrorNotes', errorNotes);
1534
- }
1535
- if (oldInfosJSON !== JSON.stringify(infoNotes)) {
1536
- this.$emit('changeInfoNotes', infoNotes);
1537
- }
1538
- if (oldWarningsJSON !== JSON.stringify(warningNotes)) {
1539
- this.$emit('changeWarningNotes', warningNotes);
1540
- }
1541
- });
1542
-
1543
- // Attempt to capture key combos.
1544
- editor.textInput.getElement().addEventListener('keydown', (evt) => {
1545
- const isComboKeyCode = evt.keyCode === 16 // SHIFT
1546
- || evt.keyCode === 17 // CTRL
1547
- || evt.keyCode === 18 // ALT or OPT
1548
- || evt.keyCode === 91 // META
1549
- || evt.keyCode === 92 // META
1550
- || evt.keyCode === 93 // META;
1551
-
1552
- // Dont do anything if only a combo key is being pressed.
1553
- if (isComboKeyCode) return;
1554
-
1555
- // If this is really a key combo then go ahead and emit that event.
1556
- const isCombo = evt.metaKey
1557
- || evt.ctrlKey
1558
- || evt.altKey
1559
- || (evt.shiftKey && (evt.key ?? '').length > 1);
1560
- if (isCombo) this.$emit('keyCombo', evt);
1561
- });
1562
- },
1563
- template: '<div ref="editor" :style="style"></div>',
1564
- };
1565
- }
1566
-
1567
- function getPrismComponentProps() {
1568
- return {
1569
- data() {
1570
- return {
1571
- annotations: [],
1572
- };
1573
- },
1574
- props: ['code', 'isDark', 'language', 'lineNumbers', 'matchBraces'],
1575
- computed: {
1576
- html() {},
1577
- preStyle() {
1578
- return {
1579
- filter: (this.isDark ?? false) !== false
1580
- ? 'none'
1581
- : 'invert(1) hue-rotate(180deg) brightness(1.1)',
1582
- margin: 0,
1583
- paddingTop: '0.5em',
1584
- paddingBottom: '0.5em'
1585
- };
1586
- }
1587
- },
1588
- watch: {
1589
- code() { this.redraw(); },
1590
- language() { this.redraw(); },
1591
- lineNumbers() { this.redraw(); },
1592
- matchBraces() { this.redraw(); },
1593
- },
1594
- methods: {
1595
- async redraw() {
1596
- /** @type {HTMLPreElement} */
1597
- const preElem = this.$refs.pre;
1598
- /** @type {HTMLElement} */
1599
- const codeElem = this.$refs.code;
1600
-
1601
- codeElem.textContent = this.code;
1602
-
1603
- codeElem.className = `language-javascript`;
1604
- preElem.className = '';
1605
-
1606
- for (const propName of ['lineNumbers', 'matchBraces']) {
1607
- if (this[propName] != null) {
1608
- const dashedPropName = propName.replace(/[A-Z]+/g, '-$&').toLowerCase();
1609
- preElem.className += ' ' + dashedPropName;
1905
+ methods: {
1906
+ async redraw() {
1907
+ /** @type {HTMLPreElement} */
1908
+ const preElem = this.$refs.pre;
1909
+ /** @type {HTMLElement} */
1910
+ const codeElem = this.$refs.code;
1911
+
1912
+ codeElem.textContent = this.code;
1913
+
1914
+ codeElem.className = `language-javascript`;
1915
+ preElem.className = '';
1916
+
1917
+ for (const propName of ['lineNumbers', 'matchBraces']) {
1918
+ if (this[propName] != null) {
1919
+ const dashedPropName = propName.replace(/[A-Z]+/g, '-$&').toLowerCase();
1920
+ preElem.className += ' ' + dashedPropName;
1921
+ }
1610
1922
  }
1923
+
1924
+ // Wait for all scripts to load before trying to syntax highlight the
1925
+ // code element.
1926
+ Prism.highlightElement(codeElem);
1611
1927
  }
1612
-
1613
- // Wait for all scripts to load before trying to syntax highlight the
1614
- // code element.
1615
- Prism.highlightElement(codeElem);
1616
- }
1617
- },
1618
- async mounted() {
1619
- await this.redraw();
1620
- },
1621
- template: '<pre ref="pre" :style="preStyle"><code ref="code"></code></pre>',
1622
- };
1623
- }
1624
-
1625
- function getIconComponentProps() {
1626
- return {
1627
- props: ['name'],
1628
- computed: {
1629
- svgCode() {
1630
- const codes = {
1631
- // https://icon-sets.iconify.design/carbon/unknown/
1632
- missing: '<svg viewBox="0 0 32 32"><circle cx="16" cy="22.5" r="1.5" fill="currentColor"/><path fill="currentColor" d="M17 19h-2v-4h2c1.103 0 2-.897 2-2s-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.5h-2V13c0-2.206 1.794-4 4-4h2c2.206 0 4 1.794 4 4s-1.794 4-4 4z"/><path fill="currentColor" d="M29.391 14.527L17.473 2.609A2.078 2.078 0 0 0 16 2c-.533 0-1.067.203-1.473.609L2.609 14.527C2.203 14.933 2 15.466 2 16s.203 1.067.609 1.473L14.526 29.39c.407.407.941.61 1.474.61s1.067-.203 1.473-.609L29.39 17.474c.407-.407.61-.94.61-1.474s-.203-1.067-.609-1.473M16 28.036L3.965 16L16 3.964L28.036 16z"/></svg>',
1633
- // https://icon-sets.iconify.design/radix-icons/trash/
1634
- trash: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M5.5 1a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1zM3 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H11v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4h-.5a.5.5 0 0 1-.5-.5M5 4h5v8H5z" clip-rule="evenodd"/></svg>',
1635
- // https://icon-sets.iconify.design/radix-icons/play/
1636
- play: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M3.242 2.322a.5.5 0 0 1 .491-.014l9 4.75a.5.5 0 0 1 0 .884l-9 4.75A.5.5 0 0 1 3 12.25v-9.5a.5.5 0 0 1 .242-.428M4 3.579v7.842L11.429 7.5z" clip-rule="evenodd"/></svg>',
1637
- // https://icon-sets.iconify.design/radix-icons/update/
1638
- refresh: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.903 7.297c0 3.044 2.207 5.118 4.686 5.547a.521.521 0 1 1-.178 1.027C3.5 13.367.861 10.913.861 7.297c0-1.537.699-2.745 1.515-3.663c.585-.658 1.254-1.193 1.792-1.602H2.532a.5.5 0 0 1 0-1h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V2.686l-.001.002c-.572.43-1.27.957-1.875 1.638c-.715.804-1.253 1.776-1.253 2.97m11.108.406c0-3.012-2.16-5.073-4.607-5.533a.521.521 0 1 1 .192-1.024c2.874.54 5.457 2.98 5.457 6.557c0 1.537-.699 2.744-1.515 3.663c-.585.658-1.254 1.193-1.792 1.602h1.636a.5.5 0 1 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v1.845h.002c.571-.432 1.27-.958 1.874-1.64c.715-.803 1.253-1.775 1.253-2.97" clip-rule="evenodd"/></svg>',
1639
- // https://icon-sets.iconify.design/radix-icons/view-horizontal/
1640
- 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>',
1641
- // https://icon-sets.iconify.design/radix-icons/view-vertical/
1642
- 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>',
1643
- // https://icon-sets.iconify.design/vaadin/play/
1644
- play: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M2 1v14l12-7z"/></svg>',
1645
- // https://icon-sets.iconify.design/mdi/error-outline/
1646
- error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
1647
- // Icons similar to those in the browser's console.
1648
- result: '<svg viewBox="0 0 16 16"><path d="M9 4.5L5.5 8 9 11.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="8" r="1.1" fill="currentColor"/></svg>',
1649
- close: '<svg viewBox="0 0 16 16"><path d="M4 4l8 8m0-8l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',
1650
- clear: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.75 12.25l8.5-8.5" stroke="currentColor" stroke-width="1.5"/></svg>',
1651
- copyToEditor: '<svg viewBox="0 0 16 16"><path d="M6 3.5L2.5 7 6 10.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7h6.5a4 4 0 0 1 4 4v1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
1652
- spinner: '<svg viewBox="0 0 16 16" class="spin"><path d="M8 1.75a6.25 6.25 0 1 1-6.25 6.25" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>',
1653
- chevron: '<svg viewBox="0 0 16 16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1654
- consoleError: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="currentColor"/><path d="M5.5 5.5l5 5m0-5l-5 5" stroke="#fff" stroke-width="1.75" stroke-linecap="round"/></svg>',
1655
- consoleWarning: '<svg viewBox="0 0 16 16"><path d="M8 1.75L14.75 14H1.25z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 6v3.5" stroke="#202124" stroke-width="1.75" stroke-linecap="round"/><circle cx="8" cy="11.75" r="1" fill="#202124"/></svg>',
1656
- };
1657
- return (codes[this.name] ?? codes.missing).replace('<svg', '$& xmlns="http://www.w3.org/2000/svg" style="height: 1em; width: 1em; display: inline-block; transform: translateY(0.1em);"');
1658
- }
1659
- },
1660
- template: '<span v-html="svgCode"></span>'
1928
+ },
1929
+ async mounted() {
1930
+ await this.redraw();
1931
+ },
1932
+ template: '<pre ref="pre" :style="preStyle"><code ref="code"></code></pre>',
1933
+ };
1661
1934
  }
1662
- }
1663
-
1664
- function getJSValueComponentProps() {
1665
- return {
1666
- props: ['description', 'path', 'name', 'isDimName'],
1667
- data() {
1668
- return {
1669
- isExpanded: false,
1670
- hasBeenExpanded: false,
1671
- };
1672
- },
1673
- watch: {
1674
- isExpanded(newValue) {
1675
- if (newValue && !this.hasBeenExpanded) {
1676
- this.hasBeenExpanded = newValue;
1935
+
1936
+ function getIconComponentProps() {
1937
+ return {
1938
+ props: ['name'],
1939
+ computed: {
1940
+ svgCode() {
1941
+ const codes = {
1942
+ // https://icon-sets.iconify.design/carbon/unknown/
1943
+ missing: '<svg viewBox="0 0 32 32"><circle cx="16" cy="22.5" r="1.5" fill="currentColor"/><path fill="currentColor" d="M17 19h-2v-4h2c1.103 0 2-.897 2-2s-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.5h-2V13c0-2.206 1.794-4 4-4h2c2.206 0 4 1.794 4 4s-1.794 4-4 4z"/><path fill="currentColor" d="M29.391 14.527L17.473 2.609A2.078 2.078 0 0 0 16 2c-.533 0-1.067.203-1.473.609L2.609 14.527C2.203 14.933 2 15.466 2 16s.203 1.067.609 1.473L14.526 29.39c.407.407.941.61 1.474.61s1.067-.203 1.473-.609L29.39 17.474c.407-.407.61-.94.61-1.474s-.203-1.067-.609-1.473M16 28.036L3.965 16L16 3.964L28.036 16z"/></svg>',
1944
+ // https://icon-sets.iconify.design/radix-icons/trash/
1945
+ trash: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M5.5 1a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1zM3 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H11v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4h-.5a.5.5 0 0 1-.5-.5M5 4h5v8H5z" clip-rule="evenodd"/></svg>',
1946
+ // https://icon-sets.iconify.design/radix-icons/play/
1947
+ play: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M3.242 2.322a.5.5 0 0 1 .491-.014l9 4.75a.5.5 0 0 1 0 .884l-9 4.75A.5.5 0 0 1 3 12.25v-9.5a.5.5 0 0 1 .242-.428M4 3.579v7.842L11.429 7.5z" clip-rule="evenodd"/></svg>',
1948
+ // https://icon-sets.iconify.design/radix-icons/update/
1949
+ refresh: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.903 7.297c0 3.044 2.207 5.118 4.686 5.547a.521.521 0 1 1-.178 1.027C3.5 13.367.861 10.913.861 7.297c0-1.537.699-2.745 1.515-3.663c.585-.658 1.254-1.193 1.792-1.602H2.532a.5.5 0 0 1 0-1h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V2.686l-.001.002c-.572.43-1.27.957-1.875 1.638c-.715.804-1.253 1.776-1.253 2.97m11.108.406c0-3.012-2.16-5.073-4.607-5.533a.521.521 0 1 1 .192-1.024c2.874.54 5.457 2.98 5.457 6.557c0 1.537-.699 2.744-1.515 3.663c-.585.658-1.254 1.193-1.792 1.602h1.636a.5.5 0 1 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v1.845h.002c.571-.432 1.27-.958 1.874-1.64c.715-.803 1.253-1.775 1.253-2.97" clip-rule="evenodd"/></svg>',
1950
+ // https://icon-sets.iconify.design/radix-icons/view-horizontal/
1951
+ 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>',
1952
+ // https://icon-sets.iconify.design/radix-icons/view-vertical/
1953
+ 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>',
1954
+ // https://icon-sets.iconify.design/vaadin/play/
1955
+ play: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M2 1v14l12-7z"/></svg>',
1956
+ // https://icon-sets.iconify.design/mdi/error-outline/
1957
+ error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
1958
+ // Icons similar to those in the browser's console.
1959
+ fullscreen: '<svg viewBox="0 0 16 16"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1960
+ exitFullscreen: '<svg viewBox="0 0 16 16"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1961
+ more: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="1.4" fill="currentColor"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/><circle cx="12.5" cy="8" r="1.4" fill="currentColor"/></svg>',
1962
+ popOut: '<svg viewBox="0 0 16 16"><path d="M9.5 2.5h4v4M13.5 2.5L8 8M12 9.5v3a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1963
+ popIn: '<svg viewBox="0 0 16 16"><path d="M12 8.5H8V4.5M8 8.5l5.5-5.5M12 10.5v2a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1964
+ code: '<svg viewBox="0 0 16 16"><path d="M5.5 4.5L2 8l3.5 3.5M10.5 4.5L14 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1965
+ info: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.25" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 7.25v4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="8" cy="5" r="1" fill="currentColor"/></svg>',
1966
+ result: '<svg viewBox="0 0 16 16"><path d="M9 4.5L5.5 8 9 11.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="8" r="1.1" fill="currentColor"/></svg>',
1967
+ close: '<svg viewBox="0 0 16 16"><path d="M4 4l8 8m0-8l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',
1968
+ clear: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.75 12.25l8.5-8.5" stroke="currentColor" stroke-width="1.5"/></svg>',
1969
+ copyToEditor: '<svg viewBox="0 0 16 16"><path d="M6 3.5L2.5 7 6 10.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7h6.5a4 4 0 0 1 4 4v1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
1970
+ spinner: '<svg viewBox="0 0 16 16" class="spin"><path d="M8 1.75a6.25 6.25 0 1 1-6.25 6.25" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>',
1971
+ chevron: '<svg viewBox="0 0 16 16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1972
+ consoleError: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="currentColor"/><path d="M5.5 5.5l5 5m0-5l-5 5" stroke="#fff" stroke-width="1.75" stroke-linecap="round"/></svg>',
1973
+ consoleWarning: '<svg viewBox="0 0 16 16"><path d="M8 1.75L14.75 14H1.25z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 6v3.5" stroke="#202124" stroke-width="1.75" stroke-linecap="round"/><circle cx="8" cy="11.75" r="1" fill="#202124"/></svg>',
1974
+ };
1975
+ return (codes[this.name] ?? codes.missing).replace('<svg', '$& xmlns="http://www.w3.org/2000/svg" style="height: 1em; width: 1em; display: inline-block; transform: translateY(0.1em);"');
1677
1976
  }
1678
- }
1679
- },
1680
- computed: {
1681
- isPartialDescription() {
1682
- return this.description.entries === undefined;
1683
1977
  },
1684
- isExpandable() {
1685
- return !this.description.isPrimitive;
1978
+ template: '<span v-html="svgCode"></span>'
1979
+ }
1980
+ }
1981
+
1982
+ function getJSValueComponentProps() {
1983
+ return {
1984
+ props: ['description', 'path', 'name', 'isDimName'],
1985
+ data() {
1986
+ return {
1987
+ isExpanded: false,
1988
+ hasBeenExpanded: false,
1989
+ };
1686
1990
  },
1687
- isEntry() {
1688
- return this.name != null;
1991
+ watch: {
1992
+ isExpanded(newValue) {
1993
+ if (newValue && !this.hasBeenExpanded) {
1994
+ this.hasBeenExpanded = newValue;
1995
+ }
1996
+ }
1689
1997
  },
1690
- classNames() {
1691
- return [
1692
- 'js-value',
1693
- // Entries are always on their own line just like expanded values.
1694
- (this.isEntry || (this.isExpandable && this.isExpanded)) ? 'd-block' : 'd-inline-block',
1695
- this.isExpanded ? 'expanded' : '',
1696
- ];
1998
+ computed: {
1999
+ isPartialDescription() {
2000
+ return this.description.entries === undefined;
2001
+ },
2002
+ isExpandable() {
2003
+ return !this.description.isPrimitive;
2004
+ },
2005
+ isEntry() {
2006
+ return this.name != null;
2007
+ },
2008
+ classNames() {
2009
+ return [
2010
+ 'js-value',
2011
+ // Entries are always on their own line just like expanded values.
2012
+ (this.isEntry || (this.isExpandable && this.isExpanded)) ? 'd-block' : 'd-inline-block',
2013
+ this.isExpanded ? 'expanded' : '',
2014
+ ];
2015
+ },
2016
+ entryGroups() {
2017
+ // protoEntries only ever contains the [[Prototype]] entry.
2018
+ return [
2019
+ { key: 'entries', isDim: entry => entry[2] === false },
2020
+ { key: 'protoEntries', isDim: () => true },
2021
+ ];
2022
+ }
1697
2023
  },
1698
- entryGroups() {
1699
- // protoEntries only ever contains the [[Prototype]] entry.
1700
- return [
1701
- { key: 'entries', isDim: entry => entry[2] === false },
1702
- { key: 'protoEntries', isDim: () => true },
1703
- ];
1704
- }
1705
- },
1706
- methods: {
1707
- toggleExpanded() {
1708
- if (!this.isExpandable) return;
1709
- this.isExpanded = !this.isExpanded;
1710
- if (this.isExpanded && this.isPartialDescription) {
1711
- messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2024
+ methods: {
2025
+ toggleExpanded() {
2026
+ if (!this.isExpandable) return;
2027
+ this.isExpanded = !this.isExpanded;
2028
+ if (this.isExpanded && this.isPartialDescription) {
2029
+ messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
2030
+ }
1712
2031
  }
1713
- }
1714
- },
1715
- template: `
1716
- <div :class="classNames">
1717
- <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
1718
- v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
1719
- v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
1720
- v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
1721
- <div v-if="hasBeenExpanded" v-show="isExpanded" class="expansion">
1722
- <div v-if="isPartialDescription" class="loading">Loading&hellip;</div>
1723
- <template v-else>
1724
- <template v-for="group in entryGroups">
1725
- <js-value
1726
- v-for="(entry, entryIndex) in description[group.key]"
1727
- :name="entry[0]"
1728
- :is-dim-name="group.isDim(entry)"
1729
- :description="entry[1]"
1730
- :path="path.concat([group.key, entryIndex])">
1731
- </js-value>
2032
+ },
2033
+ template: `
2034
+ <div :class="classNames">
2035
+ <div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
2036
+ v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
2037
+ v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
2038
+ v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
2039
+ <div v-if="hasBeenExpanded" v-show="isExpanded" class="expansion">
2040
+ <div v-if="isPartialDescription" class="loading">Loading&hellip;</div>
2041
+ <template v-else>
2042
+ <template v-for="group in entryGroups">
2043
+ <js-value
2044
+ v-for="(entry, entryIndex) in description[group.key]"
2045
+ :name="entry[0]"
2046
+ :is-dim-name="group.isDim(entry)"
2047
+ :description="entry[1]"
2048
+ :path="path.concat([group.key, entryIndex])">
2049
+ </js-value>
2050
+ </template>
1732
2051
  </template>
1733
- </template>
2052
+ </div>
1734
2053
  </div>
1735
- </div>
1736
- `
1737
- };
1738
- }
1739
-
1740
- /**
1741
- * @param {string} jsCode
1742
- * @returns {{headerLines: string, lines: string, allLines: string}[]}
1743
- */
1744
- function parseJSCodeGroups(jsCode) {
1745
- return jsCode.split(/\r\n|\r|\n/).reduce(
1746
- (groups, line) => {
1747
- const lastGroup = groups[groups.length - 1];
1748
- const isHeaderLine = /^\/\/[^]*\\\\$/.test(line);
1749
- const headerLine = isHeaderLine ? line.replace(/^\/+\s*|\s*\\+$/g, '') : line;
1750
- if (!groups.length || (isHeaderLine && !lastGroup.canAppendHeader)) {
1751
- groups.push({
1752
- headerLines: isHeaderLine ? [headerLine] : [],
1753
- lines: isHeaderLine ? [] : [line],
1754
- allLines: [line],
1755
- canAppendHeader: isHeaderLine,
1756
- });
1757
- }
1758
- else if (isHeaderLine) {
1759
- lastGroup.headerLines.push(headerLine);
1760
- lastGroup.allLines.push(line);
2054
+ `
2055
+ };
2056
+ }
2057
+
2058
+ /**
2059
+ * @param {string} jsCode
2060
+ * @returns {{headerLines: string, lines: string, allLines: string}[]}
2061
+ */
2062
+ function parseJSCodeGroups(jsCode) {
2063
+ return jsCode.split(/\r\n|\r|\n/).reduce(
2064
+ (groups, line) => {
2065
+ const lastGroup = groups[groups.length - 1];
2066
+ const isHeaderLine = /^\/\/[^]*\\\\$/.test(line);
2067
+ const headerLine = isHeaderLine ? line.replace(/^\/+\s*|\s*\\+$/g, '') : line;
2068
+ if (!groups.length || (isHeaderLine && !lastGroup.canAppendHeader)) {
2069
+ groups.push({
2070
+ headerLines: isHeaderLine ? [headerLine] : [],
2071
+ lines: isHeaderLine ? [] : [line],
2072
+ allLines: [line],
2073
+ canAppendHeader: isHeaderLine,
2074
+ });
2075
+ }
2076
+ else if (isHeaderLine) {
2077
+ lastGroup.headerLines.push(headerLine);
2078
+ lastGroup.allLines.push(line);
2079
+ }
2080
+ else {
2081
+ lastGroup.canAppendHeader = false;
2082
+ lastGroup.lines.push(line);
2083
+ lastGroup.allLines.push(line);
2084
+ }
2085
+ return groups;
2086
+ },
2087
+ []
2088
+ ).reduce(
2089
+ (groups, group) => {
2090
+ if (!group.canAppendHeader) {
2091
+ delete group.canAppendHeader;
2092
+ group.headerLines = group.headerLines.join('\n').trimEnd();
2093
+ group.lines = group.lines.join('\n');
2094
+ group.allLines = group.allLines.join('\n');
2095
+ groups.push(group);
2096
+ }
2097
+ return groups;
2098
+ },
2099
+ []
2100
+ );
2101
+ }
2102
+
2103
+ /**
2104
+ * Determines the header that should be displayed for a group of code and
2105
+ * whether or not the group's code should be hidden. A group is hidden if its
2106
+ * header starts with `hidePrefix`, in which case anything after the prefix (and
2107
+ * an optional colon) is used as the header.
2108
+ * @param {string} headerLines
2109
+ * @param {string} hidePrefix
2110
+ * @returns {{header: string, isHidden: boolean}}
2111
+ */
2112
+ function parseGroupHeader(headerLines, hidePrefix) {
2113
+ const isHidden = !!hidePrefix && headerLines.startsWith(hidePrefix);
2114
+ return {
2115
+ header: isHidden
2116
+ ? headerLines.slice(hidePrefix.length).replace(/^\s*:?\s*/, '')
2117
+ : headerLines,
2118
+ isHidden,
2119
+ };
2120
+ }
2121
+
2122
+ /**
2123
+ * Separates the hidden groups of code from the visible code.
2124
+ * @param {string} jsCode
2125
+ * @param {string} hidePrefix
2126
+ * @returns {{
2127
+ * visibleCode: string,
2128
+ * hiddenGroups: (ReturnType<parseJSCodeGroups>[number] & {runCount: number})[]
2129
+ * }}
2130
+ * `visibleCode` is the code to show in the editor. Each hidden group has a
2131
+ * `runCount` indicating how many visible groups must be run before it runs.
2132
+ */
2133
+ function extractHiddenGroups(jsCode, hidePrefix) {
2134
+ const visibleGroups = [];
2135
+ const hiddenGroups = [];
2136
+ for (const group of parseJSCodeGroups(jsCode)) {
2137
+ if (parseGroupHeader(group.headerLines, hidePrefix).isHidden) {
2138
+ hiddenGroups.push({...group, runCount: visibleGroups.length});
1761
2139
  }
1762
2140
  else {
1763
- lastGroup.canAppendHeader = false;
1764
- lastGroup.lines.push(line);
1765
- lastGroup.allLines.push(line);
1766
- }
1767
- return groups;
1768
- },
1769
- []
1770
- ).reduce(
1771
- (groups, group) => {
1772
- if (!group.canAppendHeader) {
1773
- delete group.canAppendHeader;
1774
- group.headerLines = group.headerLines.join('\n').trimEnd();
1775
- group.lines = group.lines.join('\n');
1776
- group.allLines = group.allLines.join('\n');
1777
- groups.push(group);
2141
+ visibleGroups.push(group);
1778
2142
  }
1779
- return groups;
1780
- },
1781
- []
1782
- );
1783
- }
1784
-
1785
- /**
1786
- * Determines the header that should be displayed for a group of code and
1787
- * whether or not the group's code should be hidden. A group is hidden if its
1788
- * header starts with `hidePrefix`, in which case anything after the prefix (and
1789
- * an optional colon) is used as the header.
1790
- * @param {string} headerLines
1791
- * @param {string} hidePrefix
1792
- * @returns {{header: string, isHidden: boolean}}
1793
- */
1794
- function parseGroupHeader(headerLines, hidePrefix) {
1795
- const isHidden = !!hidePrefix && headerLines.startsWith(hidePrefix);
1796
- return {
1797
- header: isHidden
1798
- ? headerLines.slice(hidePrefix.length).replace(/^\s*:?\s*/, '')
1799
- : headerLines,
1800
- isHidden,
1801
- };
1802
- }
1803
-
1804
- /**
1805
- * Separates the hidden groups of code from the visible code.
1806
- * @param {string} jsCode
1807
- * @param {string} hidePrefix
1808
- * @returns {{
1809
- * visibleCode: string,
1810
- * hiddenGroups: (ReturnType<parseJSCodeGroups>[number] & {runCount: number})[]
1811
- * }}
1812
- * `visibleCode` is the code to show in the editor. Each hidden group has a
1813
- * `runCount` indicating how many visible groups must be run before it runs.
1814
- */
1815
- function extractHiddenGroups(jsCode, hidePrefix) {
1816
- const visibleGroups = [];
1817
- const hiddenGroups = [];
1818
- for (const group of parseJSCodeGroups(jsCode)) {
1819
- if (parseGroupHeader(group.headerLines, hidePrefix).isHidden) {
1820
- hiddenGroups.push({...group, runCount: visibleGroups.length});
1821
2143
  }
1822
- else {
1823
- visibleGroups.push(group);
2144
+ return {
2145
+ visibleCode: visibleGroups.map(g => g.allLines).join('\n'),
2146
+ hiddenGroups,
2147
+ };
2148
+ }
2149
+
2150
+ /**
2151
+ * Finds the last statement in the code if it is an expression so that its
2152
+ * value can be shown.
2153
+ * @param {string} code
2154
+ * @param {"classic"|"module"} blockType
2155
+ * @returns {[number, number]|null}
2156
+ * The start and end index of the expression or `null` if the last statement
2157
+ * isn't an expression (or the code can't be parsed, in which case running
2158
+ * it will show the syntax error).
2159
+ */
2160
+ function findLastExpression(code, blockType) {
2161
+ if (!window.acorn) return null;
2162
+ try {
2163
+ const {body} = acorn.parse(code, {
2164
+ ecmaVersion: 'latest',
2165
+ sourceType: blockType === 'module' ? 'module' : 'script',
2166
+ allowHashBang: true,
2167
+ });
2168
+ const last = body[body.length - 1];
2169
+ if (last?.type === 'ExpressionStatement' && !last.directive) {
2170
+ return [last.expression.start, last.expression.end];
2171
+ }
1824
2172
  }
2173
+ catch (e) {}
2174
+ return null;
1825
2175
  }
1826
- return {
1827
- visibleCode: visibleGroups.map(g => g.allLines).join('\n'),
1828
- hiddenGroups,
1829
- };
1830
- }
1831
-
1832
- /**
1833
- * Finds the last statement in the code if it is an expression so that its
1834
- * value can be shown.
1835
- * @param {string} code
1836
- * @param {"classic"|"module"} blockType
1837
- * @returns {[number, number]|null}
1838
- * The start and end index of the expression or `null` if the last statement
1839
- * isn't an expression (or the code can't be parsed, in which case running
1840
- * it will show the syntax error).
1841
- */
1842
- function findLastExpression(code, blockType) {
1843
- if (!window.acorn) return null;
1844
- try {
1845
- const {body} = acorn.parse(code, {
1846
- ecmaVersion: 'latest',
1847
- sourceType: blockType === 'module' ? 'module' : 'script',
1848
- allowHashBang: true,
1849
- });
1850
- const last = body[body.length - 1];
1851
- if (last?.type === 'ExpressionStatement' && !last.directive) {
1852
- return [last.expression.start, last.expression.end];
2176
+
2177
+ /**
2178
+ * Joins groups of code back together making sure that each one (other than
2179
+ * the first) starts with a header so that they stay separate groups.
2180
+ * @param {string[]} blocks
2181
+ * @returns {string}
2182
+ */
2183
+ function joinCodeBlocks(blocks) {
2184
+ return blocks
2185
+ .map(block => block.replace(/^(\s*[\r\n])+|\s+$/g, ''))
2186
+ .filter(Boolean)
2187
+ .map((block, index) => {
2188
+ const firstLine = block.split(/\r\n|\r|\n/)[0];
2189
+ return index && !/^\/\/[^]*\\\\$/.test(firstLine) ? `// \\\\\n${block}` : block;
2190
+ })
2191
+ .join('\n\n');
2192
+ }
2193
+
2194
+ /**
2195
+ * Builds the HTML for a console that uses the given code and data attributes.
2196
+ * @param {Object} options
2197
+ * @param {string} options.code
2198
+ * @param {{[key: string]: string}} options.dataset
2199
+ * @param {{name: string, version: string}} options.packageInfo
2200
+ * @param {boolean} options.isFullPage
2201
+ * If `true` a full page is returned, otherwise just a snippet to embed.
2202
+ * @returns {string}
2203
+ */
2204
+ function buildConsoleHtml({code, dataset, packageInfo, isFullPage}) {
2205
+ const {name, version} = packageInfo;
2206
+ const src = `https://cdn.jsdelivr.net/npm/${name}@${version.split('.')[0]}/dist/${name}.min.js`;
2207
+ const attrs = Object.entries(dataset)
2208
+ .map(([key, value]) => ` data-${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}="${escapeHtmlAttribute(value)}"`)
2209
+ .join('');
2210
+ const indent = isFullPage ? ' ' : ' ';
2211
+
2212
+ // Keeps the code from ending the script tag early.
2213
+ const safeCode = code.replace(/<\/(script)/gi, '<\\/$1').replace(/<!--/g, '<\\!--');
2214
+ const scriptLines = safeCode.trim()
2215
+ ? [
2216
+ `<script src="${src}"${attrs}>`,
2217
+ ...safeCode.split('\n').map(line => line.trim() ? ' ' + line : ''),
2218
+ '</script>',
2219
+ ]
2220
+ : [`<script src="${src}"${attrs}></script>`];
2221
+ const indentedScript = scriptLines.map(line => line ? indent + line : line).join('\n');
2222
+
2223
+ return isFullPage
2224
+ ? [
2225
+ '<!DOCTYPE html>',
2226
+ '<html lang="en">',
2227
+ ' <head>',
2228
+ ' <meta charset="utf-8">',
2229
+ ' <meta name="viewport" content="width=device-width, initial-scale=1">',
2230
+ ' <title>JS Box</title>',
2231
+ ' <style>',
2232
+ ' html, body { height: 100%; margin: 0; }',
2233
+ ' </style>',
2234
+ ' </head>',
2235
+ ' <body>',
2236
+ indentedScript,
2237
+ ' </body>',
2238
+ '</html>',
2239
+ '',
2240
+ ].join('\n')
2241
+ : `<div style="height: 400px;">\n${indentedScript}\n</div>\n`;
2242
+ }
2243
+
2244
+ /**
2245
+ * @param {string} value
2246
+ * @returns {string}
2247
+ */
2248
+ function escapeHtmlAttribute(value) {
2249
+ return `${value}`.replace(/[&"<>]/g, c => `&#${c.charCodeAt(0)};`);
2250
+ }
2251
+
2252
+ /**
2253
+ * Finds the minimum indentation of all of the lines that have non-space
2254
+ * characters and removes the indentation accordingly for all indented lines.
2255
+ * @param {string} text
2256
+ * The string containing the lines of text that should be unindented.
2257
+ * @param {{trim: boolean, tabSize: number}=} opt_options
2258
+ * Optional, defaults to `{trim: true, tabSize: 4}`. The `trim` property
2259
+ * indicates if leading lines should be removed along with trailing
2260
+ * whitespaces. The `tabSize` property indicates how many spaces will be
2261
+ * used to replace all tab characters.
2262
+ * @returns {string}
2263
+ * A new version of `text` with all of the minimally indented lines having
2264
+ * no leading spacing and all other indented lines following suit. If
2265
+ * `opt_options.trim` is `true` all leading lines and trailing spaces will
2266
+ * not exist. All tab characters will be replaced with
2267
+ * `opt_options.tabSize` amount of space characters.
2268
+ */
2269
+ function unindentMin(text, opt_options) {
2270
+ opt_options = Object(opt_options);
2271
+ const tabSize = opt_options.tabSize ?? 4;
2272
+ const trim = opt_options.trim ?? true;
2273
+ text = text.replace(/\t/g, ' '.repeat(tabSize));
2274
+ if (!/(^|[\r\n])\S/.test(text)) {
2275
+ const rgx = /(^|[\r\n])((?:(?!\r|\n)\s)+)(?=(\S)?)/g;
2276
+ let min = Infinity;
2277
+ for (let match; match = rgx.exec(text);) {
2278
+ if (match[3]) {
2279
+ min = Math.min(min, match[2].length);
2280
+ }
2281
+ }
2282
+ text = text.replace(
2283
+ rgx,
2284
+ (_, start, spaces) => start + spaces.slice(min)
2285
+ );
1853
2286
  }
2287
+ return trim ? text.replace(/^(\s*[\r\n]+)+|\s+$/g, '') : text;
1854
2288
  }
1855
- catch (e) {}
1856
- return null;
1857
- }
1858
-
1859
- /**
1860
- * Joins groups of code back together making sure that each one (other than
1861
- * the first) starts with a header so that they stay separate groups.
1862
- * @param {string[]} blocks
1863
- * @returns {string}
1864
- */
1865
- function joinCodeBlocks(blocks) {
1866
- return blocks
1867
- .map(block => block.replace(/^(\s*[\r\n])+|\s+$/g, ''))
1868
- .filter(Boolean)
1869
- .map((block, index) => {
1870
- const firstLine = block.split(/\r\n|\r|\n/)[0];
1871
- return index && !/^\/\/[^]*\\\\$/.test(firstLine) ? `// \\\\\n${block}` : block;
1872
- })
1873
- .join('\n\n');
1874
- }
1875
-
1876
- /**
1877
- * Builds the HTML for a console that uses the given code and data attributes.
1878
- * @param {Object} options
1879
- * @param {string} options.code
1880
- * @param {{[key: string]: string}} options.dataset
1881
- * @param {{name: string, version: string}} options.packageInfo
1882
- * @param {boolean} options.isFullPage
1883
- * If `true` a full page is returned, otherwise just a snippet to embed.
1884
- * @returns {string}
1885
- */
1886
- function buildConsoleHtml({code, dataset, packageInfo, isFullPage}) {
1887
- const {name, version} = packageInfo;
1888
- const src = `https://cdn.jsdelivr.net/npm/${name}@${version.split('.')[0]}/dist/${name}.min.js`;
1889
- const attrs = Object.entries(dataset)
1890
- .map(([key, value]) => ` data-${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}="${escapeHtmlAttribute(value)}"`)
1891
- .join('');
1892
- const indent = isFullPage ? ' ' : ' ';
1893
-
1894
- // Keeps the code from ending the script tag early.
1895
- const safeCode = code.replace(/<\/(script)/gi, '<\\/$1').replace(/<!--/g, '<\\!--');
1896
- const scriptLines = safeCode.trim()
1897
- ? [
1898
- `<script src="${src}"${attrs}>`,
1899
- ...safeCode.split('\n').map(line => line.trim() ? ' ' + line : ''),
1900
- '</script>',
1901
- ]
1902
- : [`<script src="${src}"${attrs}></script>`];
1903
- const indentedScript = scriptLines.map(line => line ? indent + line : line).join('\n');
1904
-
1905
- return isFullPage
1906
- ? [
1907
- '<!DOCTYPE html>',
1908
- '<html lang="en">',
1909
- ' <head>',
1910
- ' <meta charset="utf-8">',
1911
- ' <meta name="viewport" content="width=device-width, initial-scale=1">',
1912
- ' <title>JS Box</title>',
1913
- ' <style>',
1914
- ' html, body { height: 100%; margin: 0; }',
1915
- ' </style>',
1916
- ' </head>',
1917
- ' <body>',
1918
- indentedScript,
1919
- ' </body>',
1920
- '</html>',
1921
- '',
1922
- ].join('\n')
1923
- : `<div style="height: 400px;">\n${indentedScript}\n</div>\n`;
1924
- }
1925
-
1926
- /**
1927
- * @param {string} value
1928
- * @returns {string}
1929
- */
1930
- function escapeHtmlAttribute(value) {
1931
- return `${value}`.replace(/[&"<>]/g, c => `&#${c.charCodeAt(0)};`);
1932
- }
1933
-
1934
- /**
1935
- * Finds the minimum indentation of all of the lines that have non-space
1936
- * characters and removes the indentation accordingly for all indented lines.
1937
- * @param {string} text
1938
- * The string containing the lines of text that should be unindented.
1939
- * @param {{trim: boolean, tabSize: number}=} opt_options
1940
- * Optional, defaults to `{trim: true, tabSize: 4}`. The `trim` property
1941
- * indicates if leading lines should be removed along with trailing
1942
- * whitespaces. The `tabSize` property indicates how many spaces will be
1943
- * used to replace all tab characters.
1944
- * @returns {string}
1945
- * A new version of `text` with all of the minimally indented lines having
1946
- * no leading spacing and all other indented lines following suit. If
1947
- * `opt_options.trim` is `true` all leading lines and trailing spaces will
1948
- * not exist. All tab characters will be replaced with
1949
- * `opt_options.tabSize` amount of space characters.
1950
- */
1951
- function unindentMin(text, opt_options) {
1952
- opt_options = Object(opt_options);
1953
- const tabSize = opt_options.tabSize ?? 4;
1954
- const trim = opt_options.trim ?? true;
1955
- text = text.replace(/\t/g, ' '.repeat(tabSize));
1956
- if (!/(^|[\r\n])\S/.test(text)) {
1957
- const rgx = /(^|[\r\n])((?:(?!\r|\n)\s)+)(?=(\S)?)/g;
1958
- let min = Infinity;
1959
- for (let match; match = rgx.exec(text);) {
1960
- if (match[3]) {
1961
- min = Math.min(min, match[2].length);
2289
+
2290
+ /**
2291
+ * Picks the properties of a description sent by the runner so that nothing
2292
+ * unexpected makes its way into the displays.
2293
+ * @param {*} description
2294
+ */
2295
+ function sanitizeDescription(description) {
2296
+ description = Object(description);
2297
+ return {
2298
+ typeName: `${description.typeName}`,
2299
+ isPrimitive: !!description.isPrimitive,
2300
+ // Each part is [kind, text] where kind is used in a class name.
2301
+ parts: Array.from(description.parts ?? [], part => [
2302
+ /^[a-z]+$/.test(Object(part)[0]) ? part[0] : 'text',
2303
+ `${Object(part)[1]}`,
2304
+ ]),
2305
+ ...sanitizeEntries(description),
2306
+ };
2307
+ }
2308
+
2309
+ /**
2310
+ * Picks the entries of a description sent by the runner.
2311
+ * @param {*} description
2312
+ */
2313
+ function sanitizeEntries(description) {
2314
+ description = Object(description);
2315
+ const result = {};
2316
+ for (const key of ['entries', 'protoEntries']) {
2317
+ if (Array.isArray(description[key])) {
2318
+ // Each entry is [key, description, isEnumerable].
2319
+ result[key] = description[key].map(entry => [
2320
+ `${Object(entry)[0]}`,
2321
+ sanitizeDescription(Object(entry)[1]),
2322
+ Object(entry)[2] !== false,
2323
+ ]);
1962
2324
  }
1963
2325
  }
1964
- text = text.replace(
1965
- rgx,
1966
- (_, start, spaces) => start + spaces.slice(min)
1967
- );
2326
+ return result;
1968
2327
  }
1969
- return trim ? text.replace(/^(\s*[\r\n]+)+|\s+$/g, '') : text;
1970
- }
1971
-
1972
- /**
1973
- * Picks the properties of a description sent by the runner so that nothing
1974
- * unexpected makes its way into the displays.
1975
- * @param {*} description
1976
- */
1977
- function sanitizeDescription(description) {
1978
- description = Object(description);
1979
- return {
1980
- typeName: `${description.typeName}`,
1981
- isPrimitive: !!description.isPrimitive,
1982
- // Each part is [kind, text] where kind is used in a class name.
1983
- parts: Array.from(description.parts ?? [], part => [
1984
- /^[a-z]+$/.test(Object(part)[0]) ? part[0] : 'text',
1985
- `${Object(part)[1]}`,
1986
- ]),
1987
- ...sanitizeEntries(description),
1988
- };
1989
- }
1990
-
1991
- /**
1992
- * Picks the entries of a description sent by the runner.
1993
- * @param {*} description
1994
- */
1995
- function sanitizeEntries(description) {
1996
- description = Object(description);
1997
- const result = {};
1998
- for (const key of ['entries', 'protoEntries']) {
1999
- if (Array.isArray(description[key])) {
2000
- // Each entry is [key, description, isEnumerable].
2001
- result[key] = description[key].map(entry => [
2002
- `${Object(entry)[0]}`,
2003
- sanitizeDescription(Object(entry)[1]),
2004
- Object(entry)[2] !== false,
2005
- ]);
2328
+
2329
+ /**
2330
+ * NOTE: Called via main by the runner.
2331
+ * @param {object} options
2332
+ * @param {string} options.logId
2333
+ * @param {string} options.key
2334
+ * @param {any[]} options.descriptions
2335
+ * @param {{headers: string[], rows: {index: string, cells: any[]}[]}|null} options.table
2336
+ */
2337
+ function appendLog({logId, key, descriptions, table, groupIds, groupId, isCollapsed, stack}) {
2338
+ key = `${key}`;
2339
+ mountedApp.displays.push({
2340
+ type: 'log',
2341
+ key,
2342
+ name: key === 'result' ? 'Result' : `console.${key}`,
2343
+ descriptions: Array.from(descriptions, sanitizeDescription),
2344
+ table: table ? {
2345
+ headers: Array.from(table.headers, h => `${h}`),
2346
+ rows: Array.from(table.rows, row => ({
2347
+ index: `${row.index}`,
2348
+ cells: Array.from(row.cells, cell => cell && sanitizeDescription(cell)),
2349
+ })),
2350
+ } : null,
2351
+ // The groups (from console.group()) that this message is in.
2352
+ groupIds: Array.from(groupIds ?? [], id => `${id}`),
2353
+ // Set if this message starts a group.
2354
+ groupId: groupId != null ? `${groupId}` : null,
2355
+ isCollapsed: !!isCollapsed,
2356
+ stack: stack != null ? `${stack}` : null,
2357
+ logId: `${logId}`,
2358
+ });
2359
+ }
2360
+
2361
+ /**
2362
+ * NOTE: Called via main by the runner.
2363
+ * @param {object} options
2364
+ * @param {string} options.message
2365
+ * @param {number=} options.line
2366
+ * @param {number=} options.column
2367
+ */
2368
+ function appendError({message, line, column}) {
2369
+ mountedApp.displays.push({
2370
+ type: 'error',
2371
+ message: `${message}`,
2372
+ line: +line,
2373
+ column: +column,
2374
+ });
2375
+ }
2376
+
2377
+ /**
2378
+ * NOTE: Called via main by the runner.
2379
+ * @param {(string|number)[]} path
2380
+ * The log ID, the argument index and then pairs of entry group keys and entry
2381
+ * indices.
2382
+ * @param {*} description
2383
+ */
2384
+ function updateDescriptionFor(path, description) {
2385
+ path = Array.from(path);
2386
+ const logId = path.shift();
2387
+ const display = mountedApp.displays.find(d => d.type === 'log' && d.logId === logId);
2388
+ let level = display?.descriptions[toIndex(path.shift())];
2389
+
2390
+ for (let i = 0; level && i < path.length; i += 2) {
2391
+ const groupKey = path[i];
2392
+ if (groupKey !== 'entries' && groupKey !== 'protoEntries') return;
2393
+ level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
2006
2394
  }
2395
+
2396
+ if (level) Object.assign(level, sanitizeEntries(description));
2007
2397
  }
2008
- return result;
2009
- }
2010
-
2011
- /**
2012
- * NOTE: Called via main by the runner.
2013
- * @param {object} options
2014
- * @param {string} options.logId
2015
- * @param {string} options.key
2016
- * @param {any[]} options.descriptions
2017
- * @param {{headers: string[], rows: {index: string, cells: any[]}[]}|null} options.table
2018
- */
2019
- function appendLog({logId, key, descriptions, table, groupIds, groupId, isCollapsed, stack}) {
2020
- key = `${key}`;
2021
- mountedApp.displays.push({
2022
- type: 'log',
2023
- key,
2024
- name: key === 'result' ? 'Result' : `console.${key}`,
2025
- descriptions: Array.from(descriptions, sanitizeDescription),
2026
- table: table ? {
2027
- headers: Array.from(table.headers, h => `${h}`),
2028
- rows: Array.from(table.rows, row => ({
2029
- index: `${row.index}`,
2030
- cells: Array.from(row.cells, cell => cell && sanitizeDescription(cell)),
2031
- })),
2032
- } : null,
2033
- // The groups (from console.group()) that this message is in.
2034
- groupIds: Array.from(groupIds ?? [], id => `${id}`),
2035
- // Set if this message starts a group.
2036
- groupId: groupId != null ? `${groupId}` : null,
2037
- isCollapsed: !!isCollapsed,
2038
- stack: stack != null ? `${stack}` : null,
2039
- logId: `${logId}`,
2040
- });
2041
- }
2042
-
2043
- /**
2044
- * NOTE: Called via main by the runner.
2045
- * @param {object} options
2046
- * @param {string} options.message
2047
- * @param {number=} options.line
2048
- * @param {number=} options.column
2049
- */
2050
- function appendError({message, line, column}) {
2051
- mountedApp.displays.push({
2052
- type: 'error',
2053
- message: `${message}`,
2054
- line: +line,
2055
- column: +column,
2056
- });
2057
- }
2058
-
2059
- /**
2060
- * NOTE: Called via main by the runner.
2061
- * @param {(string|number)[]} path
2062
- * The log ID, the argument index and then pairs of entry group keys and entry
2063
- * indices.
2064
- * @param {*} description
2065
- */
2066
- function updateDescriptionFor(path, description) {
2067
- path = Array.from(path);
2068
- const logId = path.shift();
2069
- const display = mountedApp.displays.find(d => d.type === 'log' && d.logId === logId);
2070
- let level = display?.descriptions[toIndex(path.shift())];
2071
-
2072
- for (let i = 0; level && i < path.length; i += 2) {
2073
- const groupKey = path[i];
2074
- if (groupKey !== 'entries' && groupKey !== 'protoEntries') return;
2075
- level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
2398
+
2399
+ /**
2400
+ * NOTE: Called via main by the runner once it has run a group of code.
2401
+ */
2402
+ function onCodeRan() {
2403
+ mountedApp.runningCount = Math.max(0, mountedApp.runningCount - 1);
2404
+ mountedApp.runHiddenGroups();
2076
2405
  }
2077
-
2078
- if (level) Object.assign(level, sanitizeEntries(description));
2079
- }
2080
-
2081
- /**
2082
- * NOTE: Called via main by the runner once it has run a group of code.
2083
- */
2084
- function onCodeRan() {
2085
- mountedApp.runningCount = Math.max(0, mountedApp.runningCount - 1);
2086
- mountedApp.runHiddenGroups();
2087
- }
2088
-
2089
- /**
2090
- * NOTE: Called via main by the runner when console.clear() is called.
2091
- */
2092
- function clearDisplays() {
2093
- mountedApp.displays = [{type: 'notice', message: 'Console was cleared'}];
2094
- }
2095
-
2096
- /**
2097
- * Only allows non-negative integers to be used as an index.
2098
- * @param {*} value
2099
- * @returns {number|undefined}
2100
- */
2101
- function toIndex(value) {
2102
- return Number.isInteger(value) && value >= 0 ? value : undefined;
2103
- }
2104
-
2105
- },
2106
- // Exact versions are used so that a new release of a library can never
2107
- // change how an existing version of this console works. Ace and Prism
2108
- // load other files (eg. language modes) from next to these files.
2109
- jsUrls: [
2110
- libraryUrl('vue', 'dist/vue.global.prod.js'),
2111
- libraryUrl('ace-builds', 'src-noconflict/ace.js'),
2112
- libraryUrl('prismjs', 'components/prism-core.min.js'),
2113
- libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
2114
- libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
2115
- // Used to find the last expression in each block of code so that its
2116
- // value can be shown.
2117
- ...(showResults ? [libraryUrl('acorn', 'dist/acorn.js')] : []),
2118
- ],
2119
- cssUrls: [
2120
- 'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
2121
- libraryUrl('prism-themes', 'themes/prism-vsc-dark-plus.min.css'),
2122
- libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.css'),
2123
- ],
2124
- htmlAttributes: 'data-theme="' + theme + '"',
2125
- onMessage(message) {
2126
- relayMessage(message.data, {runner});
2127
- },
2128
- async onReady() {
2129
- // The dataset is passed as is so that the viewer knows which options
2130
- // were actually specified (eg. when copying the console as HTML).
2131
- this.call('init', script.textContent, dataset, {runnerMode, blockType, showResults, packageInfo: PACKAGE_INFO, libraryVersions: LIBRARY_VERSIONS});
2132
- },
2133
- body: VIEWER_IFRAME_HTML,
2134
- style: {
2135
- width: '100%',
2136
- height: '100%',
2137
- border: 0,
2138
- display: 'block',
2139
- },
2140
- });
2406
+
2407
+ /**
2408
+ * NOTE: Called by main when the console is popped out into a separate window
2409
+ * or brought back into the page.
2410
+ * @param {boolean} isPoppedOut
2411
+ * @param {*=} state
2412
+ * When brought back, the state of the pop-out window's viewer (if it sent
2413
+ * it).
2414
+ */
2415
+ function setPoppedOut(isPoppedOut, state) {
2416
+ mountedApp.isPoppedOut = isPoppedOut;
2417
+ if (state) mountedApp.restoreState(state);
2418
+ }
2419
+
2420
+ /**
2421
+ * NOTE: Called by main to ask the pop-out window to send its state back.
2422
+ */
2423
+ function popIn() {
2424
+ mountedApp.bringBack();
2425
+ }
2426
+
2427
+ /**
2428
+ * NOTE: Called by main if the browser blocked the pop-out window.
2429
+ */
2430
+ function onPopOutFailed() {
2431
+ mountedApp.displays.push({
2432
+ type: 'notice',
2433
+ message: 'The pop-out window was blocked by the browser. Allow pop-ups for this site and try again.',
2434
+ });
2435
+ }
2436
+
2437
+ /**
2438
+ * NOTE: Called via main by the runner when console.clear() is called.
2439
+ */
2440
+ function clearDisplays() {
2441
+ mountedApp.displays = [{type: 'notice', message: 'Console was cleared'}];
2442
+ }
2443
+
2444
+ /**
2445
+ * Only allows non-negative integers to be used as an index.
2446
+ * @param {*} value
2447
+ * @returns {number|undefined}
2448
+ */
2449
+ function toIndex(value) {
2450
+ return Number.isInteger(value) && value >= 0 ? value : undefined;
2451
+ }
2452
+
2453
+ },
2454
+ // Exact versions are used so that a new release of a library can never
2455
+ // change how an existing version of this console works. Ace and Prism
2456
+ // load other files (eg. language modes) from next to these files.
2457
+ jsUrls: [
2458
+ libraryUrl('vue', 'dist/vue.global.prod.js'),
2459
+ libraryUrl('ace-builds', 'src-noconflict/ace.js'),
2460
+ libraryUrl('prismjs', 'components/prism-core.min.js'),
2461
+ libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
2462
+ libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
2463
+ // Used to find the last expression in each block of code so that its
2464
+ // value can be shown.
2465
+ ...(showResults ? [libraryUrl('acorn', 'dist/acorn.js')] : []),
2466
+ ],
2467
+ cssUrls: [
2468
+ 'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
2469
+ libraryUrl('prism-themes', 'themes/prism-vsc-dark-plus.min.css'),
2470
+ libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.css'),
2471
+ ],
2472
+ head: targetWindow ? '<meta charset="utf-8"><title>JS Box</title>' : '',
2473
+ htmlAttributes: 'data-theme="' + theme + '"',
2474
+ targetWindow,
2475
+ onMessage(message) {
2476
+ relayMessage(message.data, {runner, host: {apply: (func, args) => hostFuncs[func](...args)}});
2477
+ },
2478
+ async onReady() {
2479
+ // The dataset is passed as is so that the viewer knows which options
2480
+ // were actually specified (eg. when copying the console as HTML).
2481
+ this.call('init', script.textContent, dataset, {
2482
+ runnerMode,
2483
+ blockType,
2484
+ showResults,
2485
+ packageInfo: PACKAGE_INFO,
2486
+ libraryVersions: LIBRARY_VERSIONS,
2487
+ isPopOut: !!targetWindow,
2488
+ popOutState: state ?? null,
2489
+ });
2490
+ },
2491
+ body: VIEWER_IFRAME_HTML,
2492
+ style: {
2493
+ width: '100%',
2494
+ height: '100%',
2495
+ border: 0,
2496
+ display: 'block',
2497
+ },
2498
+ });
2499
+ }
2141
2500
 
2142
- script.parentNode.insertBefore(callableViewerFrame.iframe, script);
2501
+ inlineViewer = activeViewer = createViewer();
2502
+ script.parentNode.insertBefore(inlineViewer.iframe, script);
2143
2503
  }
2144
2504
 
2145
2505
  // NOTE: This solution was intentionally written without using newer JS
2146
2506
  // features to make the minified version even smaller.
2147
2507
  var createCallableFrame = (function () {
2148
2508
  var IFRAME_SCRIPT_MESSAGE_CODE = parseFunction(function() {
2509
+ // The window that created this one: the opener if this is a pop-out
2510
+ // window or else the parent of the IFRAME.
2511
+ var HOST_WINDOW = window.opener || window.parent;
2512
+
2149
2513
  // NOTE: Referencing with window to ensure that local namespace will not
2150
2514
  // interfere.
2151
2515
  window.addEventListener('message', function(e) {
2152
2516
  var data = e.data;
2153
2517
  if (
2154
- e.source === window.parent
2518
+ e.source === HOST_WINDOW
2155
2519
  && data && /^[A-Za-z_$][\w$]*$/.test(data.funcName)
2156
2520
  && Array.isArray(data.args)
2157
2521
  ) {
@@ -2161,7 +2525,7 @@
2161
2525
  });
2162
2526
 
2163
2527
  function messageParent(message) {
2164
- window.parent.postMessage(message, '*');
2528
+ HOST_WINDOW.postMessage(message, '*');
2165
2529
  }
2166
2530
  }).body;
2167
2531
 
@@ -2187,6 +2551,9 @@
2187
2551
  * The HTML code that will be used to instantiate the page.
2188
2552
  * @param {string=} options.htmlAttributes
2189
2553
  * Attributes to add to the `<html>` element of the IFRAME.
2554
+ * @param {Window=} options.targetWindow
2555
+ * If given the page is written into this (same origin) window (eg. a
2556
+ * pop-out window) instead of into a new IFRAME.
2190
2557
  * @param {((this: R, event: MessageEvent) => void)=} options.onMessage
2191
2558
  * @param {((this: R, event: MessageEvent) => void)=} options.onReady
2192
2559
  * @returns {R}
@@ -2211,11 +2578,11 @@
2211
2578
  '(function(READY_ID){',
2212
2579
  parseFunction(function() {
2213
2580
  applyParent = function(funcName, args) {
2214
- window.parent.postMessage({funcName: funcName, args: args, id: READY_ID}, '*');
2581
+ HOST_WINDOW.postMessage({funcName: funcName, args: args, id: READY_ID}, '*');
2215
2582
  };
2216
2583
 
2217
2584
  callParent = function(funcName) {
2218
- window.parent.postMessage(
2585
+ HOST_WINDOW.postMessage(
2219
2586
  {funcName: funcName, args: Array.prototype.slice.call(arguments, 1), id: READY_ID},
2220
2587
  '*'
2221
2588
  );
@@ -2235,8 +2602,12 @@
2235
2602
  // Prevents the HTML parser from ending the inline script early.
2236
2603
  .replace(/<(?=\/script|!--)/gi, '\\x3C');
2237
2604
 
2238
- // Creates the IFRAME and sets its source via srcdoc.
2239
- var IFRAME = document.createElement('iframe');
2605
+ var TARGET_WINDOW = options.targetWindow;
2606
+ var IFRAME = TARGET_WINDOW ? null : document.createElement('iframe');
2607
+ function getFrameWindow() {
2608
+ return TARGET_WINDOW || IFRAME.contentWindow;
2609
+ }
2610
+
2240
2611
  var HTML_CODE = [
2241
2612
  '<!DOCTYPE html>',
2242
2613
  '<html ' + (options.htmlAttributes || '') + '>',
@@ -2257,10 +2628,20 @@
2257
2628
  '</body>',
2258
2629
  '</html>'
2259
2630
  ].join('\n');
2260
- IFRAME.srcdoc = HTML_CODE;
2631
+ // Writes the page into the target window or sets the IFRAME's source.
2632
+ if (TARGET_WINDOW) {
2633
+ TARGET_WINDOW.document.open();
2634
+ TARGET_WINDOW.document.write(HTML_CODE);
2635
+ TARGET_WINDOW.document.close();
2636
+ }
2637
+ else {
2638
+ IFRAME.srcdoc = HTML_CODE;
2639
+ // Allows the console to go full screen.
2640
+ IFRAME.setAttribute('allow', 'fullscreen');
2641
+ }
2261
2642
 
2262
2643
  // Set the style of the iframe.
2263
- if (style) {
2644
+ if (style && IFRAME) {
2264
2645
  if ('string' === typeof style) {
2265
2646
  IFRAME.style.cssText = style;
2266
2647
  }
@@ -2274,8 +2655,8 @@
2274
2655
  }
2275
2656
 
2276
2657
  // Adds an event listener so that messages from the IFRAME will be captured.
2277
- addEventListener('message', function(e) {
2278
- if (e.source === IFRAME.contentWindow) {
2658
+ function onWindowMessage(e) {
2659
+ if (e.source === getFrameWindow()) {
2279
2660
  var data = e.data;
2280
2661
  var dataIsReadyId = READY_ID === data;
2281
2662
  if (isReady && !dataIsReadyId) {
@@ -2299,13 +2680,14 @@
2299
2680
  console.warn('Message sent from callable frame prematurely:', e);
2300
2681
  }
2301
2682
  }
2302
- });
2683
+ }
2684
+ addEventListener('message', onWindowMessage);
2303
2685
 
2304
2686
  // Returns an object which makes it possible to call functions and get
2305
2687
  // access to the IFRAME.
2306
2688
  var queuedMessages = [];
2307
2689
  function postToFrame(message) {
2308
- if (isReady) IFRAME.contentWindow.postMessage(message, '*');
2690
+ if (isReady) getFrameWindow().postMessage(message, '*');
2309
2691
  else queuedMessages.push(message);
2310
2692
  }
2311
2693
  var callableFrame = {
@@ -2315,7 +2697,12 @@
2315
2697
  call: function(funcName) {
2316
2698
  postToFrame({funcName: funcName, args: Array.prototype.slice.call(arguments, 1)});
2317
2699
  },
2318
- iframe: IFRAME
2700
+ iframe: IFRAME,
2701
+ window: TARGET_WINDOW,
2702
+ // Stops listening for messages from the frame.
2703
+ dispose: function() {
2704
+ removeEventListener('message', onWindowMessage);
2705
+ }
2319
2706
  };
2320
2707
  return callableFrame;
2321
2708
  };
@@ -2323,7 +2710,9 @@
2323
2710
  * @typedef {Object} createCallableFrame_Return
2324
2711
  * @property {(funcName: string, args: any[]) => void} apply
2325
2712
  * @property {(funcName: string, ...args: any[]) => void} call
2326
- * @property {HTMLIFrameElement} iframe
2713
+ * @property {HTMLIFrameElement?} iframe
2714
+ * @property {Window=} window
2715
+ * @property {() => void} dispose
2327
2716
  */
2328
2717
 
2329
2718
  /**