yourjs-box 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +71 -18
- package/dist/yourjs-box.full.js +776 -64
- package/dist/yourjs-box.js +16 -9
- package/dist/yourjs-box.min.js +8 -7
- package/package.json +2 -2
package/dist/yourjs-box.full.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/*! yourjs-box v1.
|
|
1
|
+
/*! yourjs-box v1.7.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
|
|
2
2
|
(() => {
|
|
3
3
|
/**
|
|
4
4
|
* Viewer IFRAME's CSS code
|
|
5
5
|
* @type {string}
|
|
6
6
|
*/
|
|
7
|
-
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
7
|
+
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.history-panel{max-width:min(420px,calc(100% - 12px));width:360px}.history-title{color:var(--muted-text);font-size:11px;font-weight:600;letter-spacing:.05em;padding:4px 8px 6px;text-transform:uppercase}.history-item{justify-content:space-between}.history-label{font-family:var(--console-font);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-detail{color:var(--muted-text);flex-shrink:0;font-size:11px}.history-empty,.history-hint{color:var(--muted-text);font-size:12px;padding:6px 8px}.history-hint{border-top:1px solid var(--toolbar-border);margin-top:4px}.value-menu{bottom:auto;min-width:220px;position:fixed;right:auto;z-index:850}.toast{animation:fade-in .15s ease-out;background-color:var(--console-text);border-radius:6px;bottom:44px;box-shadow:0 6px 20px rgb(0 0 0 / .25);color:var(--console-bg);font-size:12px;left:50%;max-width:calc(100% - 32px);padding:6px 12px;pointer-events:none;position:fixed;transform:translateX(-50%);z-index:860}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
8
8
|
/**
|
|
9
9
|
* Information about this package (eg. its version).
|
|
10
10
|
* @type {{name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}
|
|
11
11
|
*/
|
|
12
|
-
const PACKAGE_INFO = {"name":"yourjs-box","version":"1.
|
|
12
|
+
const PACKAGE_INFO = {"name":"yourjs-box","version":"1.7.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
|
|
13
13
|
/**
|
|
14
14
|
* Viewer IFRAME's HTML code
|
|
15
15
|
* @type {string}
|
|
16
16
|
*/
|
|
17
|
-
const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading…</div><div class=\"splash-message splash-error\">JS Box couldn’t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' && !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 && 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−</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save…</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License © 2023-present Christopher West · 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…</div><div class=\"splash-message splash-error\">JS Box couldn’t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.ts,.mts,.cts,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism :language=\"language\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' && !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" :language=\"language\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf && 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−</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save…</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press ↑ at the start of the editor (or ↓ at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Language</dt><dd>{{ isTypeScript ? 'TypeScript (types are removed by Babel, not checked)' : 'JavaScript' }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>↑</kbd> / <kbd>↓</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '⌥' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License © 2023-present Chris West · Built with {{ builtWith }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
|
|
18
18
|
/**
|
|
19
19
|
* The URL of this script which is used to remove this script's lines from the
|
|
20
20
|
* stack traces of errors in window mode.
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
* was tested with.
|
|
34
34
|
*/
|
|
35
35
|
const LIBRARY_VERSIONS = {
|
|
36
|
+
// Only loaded by TypeScript consoles.
|
|
37
|
+
'@babel/standalone': '7.29.9',
|
|
36
38
|
'ace-builds': '1.44.0',
|
|
37
39
|
'acorn': '8.18.0',
|
|
38
40
|
'prism-themes': '1.9.0',
|
|
@@ -89,8 +91,8 @@
|
|
|
89
91
|
* to these calls.
|
|
90
92
|
*/
|
|
91
93
|
const RELAYABLE_FUNCS = {
|
|
92
|
-
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
|
|
93
|
-
runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
|
|
94
|
+
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'onStoredAsGlobal', 'onValueJson', 'updateDescriptionFor'],
|
|
95
|
+
runner: ['clearLogs', 'getValueAsJson', 'refreshDescription', 'reset', 'runCode', 'sendDescriptionFor', 'storeAsGlobal'],
|
|
94
96
|
// Things that the viewer (but never the runner) can ask this script to do.
|
|
95
97
|
host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
|
|
96
98
|
};
|
|
@@ -138,6 +140,9 @@
|
|
|
138
140
|
// How wrapping the last expression of each block shifted the columns on its
|
|
139
141
|
// line so that stack traces can show the original columns.
|
|
140
142
|
const columnShiftsBySnippet = {};
|
|
143
|
+
// For TypeScript, where each position in the code that ran came from in the
|
|
144
|
+
// TypeScript code (see runCode()).
|
|
145
|
+
const sourceMapsBySnippet = {};
|
|
141
146
|
|
|
142
147
|
const counts = {};
|
|
143
148
|
const timers = {};
|
|
@@ -334,15 +339,40 @@
|
|
|
334
339
|
.split('\n')
|
|
335
340
|
.filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
|
|
336
341
|
.join('\n')
|
|
337
|
-
|
|
338
|
-
|
|
342
|
+
.replace(/(snippet-\d+\.[jt]s):(\d+):(\d+)/g, (match, name, line, column) => {
|
|
343
|
+
line = +line;
|
|
344
|
+
column = +column;
|
|
345
|
+
// Undoes the shift in columns caused by wrapping the last expression.
|
|
339
346
|
const info = columnShiftsBySnippet[name];
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
if (info && line === info.line && column > info.column) {
|
|
348
|
+
column = Math.max(info.column, column - info.shift);
|
|
349
|
+
}
|
|
350
|
+
[line, column] = toSourcePosition(sourceMapsBySnippet[name], line, column);
|
|
351
|
+
return `${name}:${line}:${column}`;
|
|
343
352
|
});
|
|
344
353
|
}
|
|
345
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Finds where a position in compiled code came from in the original code.
|
|
357
|
+
* @param {number[][][]=} sourceMap
|
|
358
|
+
* For each line of the compiled code, its mappings as [compiled column,
|
|
359
|
+
* original line, original column] (all starting at 0) sorted by column.
|
|
360
|
+
* @param {number} line
|
|
361
|
+
* @param {number} column
|
|
362
|
+
* @returns {[number, number]}
|
|
363
|
+
* The original line and column (both starting at 1) or the given ones if
|
|
364
|
+
* the position isn't mapped.
|
|
365
|
+
*/
|
|
366
|
+
function toSourcePosition(sourceMap, line, column) {
|
|
367
|
+
const mappings = sourceMap?.[line - 1];
|
|
368
|
+
let best;
|
|
369
|
+
for (const mapping of mappings ?? []) {
|
|
370
|
+
if (mapping[0] > column - 1) break;
|
|
371
|
+
best = mapping;
|
|
372
|
+
}
|
|
373
|
+
return best ? [best[1] + 1, best[2] + 1 + (column - 1 - best[0])] : [line, column];
|
|
374
|
+
}
|
|
375
|
+
|
|
346
376
|
function without(props, obj) {
|
|
347
377
|
if (!obj) return obj => without(props, obj);
|
|
348
378
|
const ret = {...obj};
|
|
@@ -631,9 +661,9 @@
|
|
|
631
661
|
const $entries = [];
|
|
632
662
|
const protoEntries = [];
|
|
633
663
|
const $protoEntries = [];
|
|
664
|
+
let wasIterated = false;
|
|
634
665
|
|
|
635
666
|
if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
|
|
636
|
-
let wasIterated = false;
|
|
637
667
|
try {
|
|
638
668
|
// Map
|
|
639
669
|
if (typeName === 'Map') {
|
|
@@ -681,7 +711,9 @@
|
|
|
681
711
|
}
|
|
682
712
|
}
|
|
683
713
|
|
|
684
|
-
|
|
714
|
+
// Map and Set entries (and other iterated values) aren't properties so
|
|
715
|
+
// they have no property path.
|
|
716
|
+
return {entries, protoEntries, entriesArePropertyKeys: !wasIterated, $entries, $protoEntries};
|
|
685
717
|
}
|
|
686
718
|
|
|
687
719
|
// ['123', 0, 'entries', 0]
|
|
@@ -707,12 +739,22 @@
|
|
|
707
739
|
* @param {[number, number]=} runOptions.resultRange
|
|
708
740
|
* The start and end index of the last expression in the code whose value
|
|
709
741
|
* should be shown.
|
|
742
|
+
* @param {"javascript"|"typescript"=} runOptions.language
|
|
743
|
+
* The language that the code was written in. TypeScript is compiled to
|
|
744
|
+
* JavaScript before it is sent here.
|
|
745
|
+
* @param {number[][][]=} runOptions.sourceMap
|
|
746
|
+
* For TypeScript, where the compiled code came from (see
|
|
747
|
+
* toSourcePosition()).
|
|
748
|
+
* @param {{message: string, line: number, column: number}=} runOptions.compileError
|
|
749
|
+
* For TypeScript, the syntax error that kept the code from being compiled
|
|
750
|
+
* which is reported instead of running the code.
|
|
710
751
|
*/
|
|
711
752
|
async function runCode(jsCode, runOptions) {
|
|
712
|
-
const {blockType, resultRange} = Object(runOptions);
|
|
753
|
+
const {blockType, resultRange, language, sourceMap, compileError} = Object(runOptions);
|
|
713
754
|
|
|
714
755
|
// Names the code so that stack traces refer to it by this name.
|
|
715
|
-
const snippetName = `snippet-${++snippetCount}
|
|
756
|
+
const snippetName = `snippet-${++snippetCount}.${language === 'typescript' ? 'ts' : 'js'}`;
|
|
757
|
+
if (Array.isArray(sourceMap)) sourceMapsBySnippet[snippetName] = sourceMap;
|
|
716
758
|
|
|
717
759
|
// Wraps the last expression so that its value is reported. This keeps
|
|
718
760
|
// everything on the same lines so that line numbers in errors still match
|
|
@@ -733,7 +775,17 @@
|
|
|
733
775
|
|
|
734
776
|
runningBlockType = blockType === 'module' ? 'module' : 'classic';
|
|
735
777
|
try {
|
|
736
|
-
if (
|
|
778
|
+
if (compileError) {
|
|
779
|
+
// Reported here so that it shows up after anything logged by the code
|
|
780
|
+
// that ran before it.
|
|
781
|
+
const {message, line, column} = compileError;
|
|
782
|
+
reportUncaught({
|
|
783
|
+
name: 'SyntaxError',
|
|
784
|
+
message: `${message}`,
|
|
785
|
+
stack: `SyntaxError: ${message}\n at ${snippetName}:${+line}:${+column}`,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
else if (blockType === 'module') {
|
|
737
789
|
// Waits for the module to finish (including any top-level await).
|
|
738
790
|
await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
|
|
739
791
|
}
|
|
@@ -792,16 +844,25 @@
|
|
|
792
844
|
* entry indices.
|
|
793
845
|
*/
|
|
794
846
|
function sendDescriptionFor(path) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
847
|
+
sendDescription(path, false);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Describes the value again (eg. after the code changed it) including its
|
|
852
|
+
* preview.
|
|
853
|
+
* @param {(string|number)[]} path
|
|
854
|
+
*/
|
|
855
|
+
function refreshDescription(path) {
|
|
856
|
+
sendDescription(path, true);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* @param {(string|number)[]} path
|
|
861
|
+
* @param {boolean} isRefresh
|
|
862
|
+
* If `true` the value's preview is included too.
|
|
863
|
+
*/
|
|
864
|
+
function sendDescription(path, isRefresh) {
|
|
865
|
+
const level = getLevel(path);
|
|
805
866
|
if (!level) return;
|
|
806
867
|
const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
|
|
807
868
|
|
|
@@ -809,13 +870,97 @@
|
|
|
809
870
|
Object.assign(level, description);
|
|
810
871
|
|
|
811
872
|
// Send the description without values back to the viewer.
|
|
873
|
+
const summary = isRefresh
|
|
874
|
+
? summarize(level.value, path.length > 2 ? 1 : 0, path[path.length - 2] === 'protoEntries')
|
|
875
|
+
: {};
|
|
812
876
|
send({
|
|
813
877
|
target: 'viewer',
|
|
814
878
|
func: 'updateDescriptionFor',
|
|
815
|
-
args: [path, without(['$entries', '$protoEntries'], description)],
|
|
879
|
+
args: [path, {...without(['$entries', '$protoEntries'], description), ...summary}],
|
|
816
880
|
});
|
|
817
881
|
}
|
|
818
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Finds what is stored for a logged value (or a value within it).
|
|
885
|
+
* @param {(string|number)[]} path
|
|
886
|
+
* The log ID, the argument index and then pairs of entry group keys and
|
|
887
|
+
* entry indices.
|
|
888
|
+
* @returns {{value: *, receiver?: *}|undefined}
|
|
889
|
+
*/
|
|
890
|
+
function getLevel(path) {
|
|
891
|
+
let level = logArgsById;
|
|
892
|
+
path.forEach((pathPart, index) => {
|
|
893
|
+
level = level?.[index && index % 2 === 0 ? '$' + pathPart : pathPart];
|
|
894
|
+
});
|
|
895
|
+
return level;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Sends a value as JSON to the viewer (eg. to copy or save it).
|
|
900
|
+
* @param {(string|number)[]} path
|
|
901
|
+
* @param {string} requestId
|
|
902
|
+
*/
|
|
903
|
+
function getValueAsJson(path, requestId) {
|
|
904
|
+
const level = getLevel(path);
|
|
905
|
+
let result;
|
|
906
|
+
try {
|
|
907
|
+
result = level ? toJson(level.value) : {error: 'The value is no longer available.'};
|
|
908
|
+
}
|
|
909
|
+
catch (e) {
|
|
910
|
+
result = {error: `The value couldn't be converted to JSON (${e?.message ?? e}).`};
|
|
911
|
+
}
|
|
912
|
+
send({target: 'viewer', func: 'onValueJson', args: [{requestId, ...result}]});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Converts a value to JSON like JSON.stringify() does except that Maps and
|
|
917
|
+
* Sets become arrays, BigInts become strings and circular references (an
|
|
918
|
+
* object inside of itself) are left out instead of causing an error. An
|
|
919
|
+
* object that appears more than once without being circular is kept.
|
|
920
|
+
* @param {*} value
|
|
921
|
+
* @returns {{json?: string, circularCount: number, error?: string}}
|
|
922
|
+
*/
|
|
923
|
+
function toJson(value) {
|
|
924
|
+
let circularCount = 0;
|
|
925
|
+
// The objects currently being converted along with what they were
|
|
926
|
+
// converted from (eg. a Map is converted to an array).
|
|
927
|
+
const stack = [];
|
|
928
|
+
const json = JSON.stringify(value, function(key, val) {
|
|
929
|
+
// `this` is the object containing `key` so anything after it on the
|
|
930
|
+
// stack has already been converted.
|
|
931
|
+
while (stack.length && stack[stack.length - 1].converted !== this) stack.pop();
|
|
932
|
+
|
|
933
|
+
if ('bigint' === typeof val) return `${val}`;
|
|
934
|
+
if (val !== null && 'object' === typeof val) {
|
|
935
|
+
const original = val;
|
|
936
|
+
if (stack.some(item => item.original === original)) {
|
|
937
|
+
circularCount++;
|
|
938
|
+
return undefined;
|
|
939
|
+
}
|
|
940
|
+
if (val instanceof Map || val instanceof Set) val = [...val];
|
|
941
|
+
stack.push({original, converted: val});
|
|
942
|
+
}
|
|
943
|
+
return val;
|
|
944
|
+
}, 2);
|
|
945
|
+
return json === undefined
|
|
946
|
+
? {error: 'This value can\'t be represented as JSON.', circularCount}
|
|
947
|
+
: {json, circularCount};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Stores a value as a global variable (temp1, temp2, etc.) like the
|
|
952
|
+
* browser's console does so that code can use it.
|
|
953
|
+
* @param {(string|number)[]} path
|
|
954
|
+
*/
|
|
955
|
+
function storeAsGlobal(path) {
|
|
956
|
+
const level = getLevel(path);
|
|
957
|
+
if (!level) return;
|
|
958
|
+
let index = 1;
|
|
959
|
+
while (`temp${index}` in globalThis) index++;
|
|
960
|
+
globalThis[`temp${index}`] = level.value;
|
|
961
|
+
send({target: 'viewer', func: 'onStoredAsGlobal', args: [`temp${index}`]});
|
|
962
|
+
}
|
|
963
|
+
|
|
819
964
|
/**
|
|
820
965
|
* Forgets all of the logged values (eg. when the console is cleared).
|
|
821
966
|
*/
|
|
@@ -824,7 +969,7 @@
|
|
|
824
969
|
groupIds = [];
|
|
825
970
|
}
|
|
826
971
|
|
|
827
|
-
return {clearLogs, destroy, runCode, sendDescriptionFor};
|
|
972
|
+
return {clearLogs, destroy, getValueAsJson, refreshDescription, runCode, sendDescriptionFor, storeAsGlobal};
|
|
828
973
|
}
|
|
829
974
|
|
|
830
975
|
|
|
@@ -950,6 +1095,7 @@
|
|
|
950
1095
|
function createConsole({code, dataset, height, insert}) {
|
|
951
1096
|
const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
|
|
952
1097
|
const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
|
|
1098
|
+
const language = dataset.language === 'typescript' ? 'typescript' : 'javascript';
|
|
953
1099
|
const showResults = dataset.showResults !== 'false';
|
|
954
1100
|
// An empty data-imports-url turns off loading packages by name.
|
|
955
1101
|
const importsUrl = dataset.importsUrl == null
|
|
@@ -1141,6 +1287,12 @@
|
|
|
1141
1287
|
*/
|
|
1142
1288
|
function init(jsCode, dataset, meta) {
|
|
1143
1289
|
if (IS_MISSING_LIBRARIES) return;
|
|
1290
|
+
// TypeScript can't run without Babel.
|
|
1291
|
+
if (meta.language === 'typescript' && !window.Babel) {
|
|
1292
|
+
document.querySelector('#splash').classList.add('failed');
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
Prism.plugins.autoloader.loadLanguages(meta.language);
|
|
1144
1296
|
|
|
1145
1297
|
const hidePrefix = dataset.hidePrefix ?? '';
|
|
1146
1298
|
const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
|
|
@@ -1164,6 +1316,7 @@
|
|
|
1164
1316
|
// The name of the file that was last opened or saved.
|
|
1165
1317
|
fileName: null,
|
|
1166
1318
|
blockType: meta.blockType,
|
|
1319
|
+
language: meta.language,
|
|
1167
1320
|
showResults: meta.showResults,
|
|
1168
1321
|
packageInfo: meta.packageInfo,
|
|
1169
1322
|
libraryVersions: meta.libraryVersions,
|
|
@@ -1199,6 +1352,18 @@
|
|
|
1199
1352
|
dividerSize: '8px',
|
|
1200
1353
|
tempDividerPct: null,
|
|
1201
1354
|
isMenuOpen: false,
|
|
1355
|
+
isHistoryOpen: false,
|
|
1356
|
+
// The code that was run (newest last) which can be brought back into
|
|
1357
|
+
// the editor like the browser's console (eg. with the up arrow).
|
|
1358
|
+
commandHistory: [],
|
|
1359
|
+
// While going through the history: the index of the code shown and
|
|
1360
|
+
// the unrun code that was in the editor (shown again at the end).
|
|
1361
|
+
historyIndex: null,
|
|
1362
|
+
historyDraft: null,
|
|
1363
|
+
/** @type {{x: number, y: number, path: any[], propertyPath: string?, isPrimitive: boolean}?} */
|
|
1364
|
+
valueMenu: null,
|
|
1365
|
+
/** @type {{message: string}?} */
|
|
1366
|
+
toast: null,
|
|
1202
1367
|
textScale: loadTextScale(),
|
|
1203
1368
|
isFullscreen: false,
|
|
1204
1369
|
// Set when full screen isn't allowed so the console fills the page.
|
|
@@ -1227,6 +1392,21 @@
|
|
|
1227
1392
|
const name = this.theme === 'dark' ? 'Dark' : 'Light';
|
|
1228
1393
|
return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
|
|
1229
1394
|
},
|
|
1395
|
+
isTypeScript() {
|
|
1396
|
+
return this.language === 'typescript';
|
|
1397
|
+
},
|
|
1398
|
+
/** The libraries (and their versions) that this console uses. */
|
|
1399
|
+
builtWith() {
|
|
1400
|
+
const versions = this.libraryVersions;
|
|
1401
|
+
const names = [
|
|
1402
|
+
`Vue ${versions.vue}`,
|
|
1403
|
+
`Ace ${versions['ace-builds']}`,
|
|
1404
|
+
`Prism ${versions.prismjs}`,
|
|
1405
|
+
`Acorn ${versions.acorn}`,
|
|
1406
|
+
...this.isTypeScript ? [`Babel ${versions['@babel/standalone']}`] : [],
|
|
1407
|
+
];
|
|
1408
|
+
return names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
|
|
1409
|
+
},
|
|
1230
1410
|
blockTypeDescription() {
|
|
1231
1411
|
return this.blockType === 'module'
|
|
1232
1412
|
? 'Modules (top-level await and imports work, declarations stay in each block)'
|
|
@@ -1321,11 +1501,23 @@
|
|
|
1321
1501
|
bottomButtons() {
|
|
1322
1502
|
const isFullscreen = this.isFullscreen || this.isMaximized;
|
|
1323
1503
|
return [
|
|
1504
|
+
{
|
|
1505
|
+
iconName: 'more',
|
|
1506
|
+
title: 'More',
|
|
1507
|
+
className: 'more-button',
|
|
1508
|
+
callback() { this.toggleMenu(); },
|
|
1509
|
+
},
|
|
1324
1510
|
{
|
|
1325
1511
|
iconName: 'clear',
|
|
1326
1512
|
title: 'Clear console',
|
|
1327
1513
|
callback() { this.clearConsole(); },
|
|
1328
1514
|
},
|
|
1515
|
+
{
|
|
1516
|
+
iconName: 'history',
|
|
1517
|
+
title: `History (${isMac ? '\u2325' : 'Alt+'}H)`,
|
|
1518
|
+
className: 'history-button',
|
|
1519
|
+
callback() { this.toggleHistory(); },
|
|
1520
|
+
},
|
|
1329
1521
|
{
|
|
1330
1522
|
iconName: 'horizontalView',
|
|
1331
1523
|
title: 'Show the editor below the console',
|
|
@@ -1343,12 +1535,6 @@
|
|
|
1343
1535
|
title: isFullscreen ? 'Exit full screen' : 'Full screen',
|
|
1344
1536
|
callback() { this.toggleFullscreen(); },
|
|
1345
1537
|
},
|
|
1346
|
-
{
|
|
1347
|
-
iconName: 'more',
|
|
1348
|
-
title: 'More',
|
|
1349
|
-
className: 'more-button',
|
|
1350
|
-
callback() { this.toggleMenu(); },
|
|
1351
|
-
},
|
|
1352
1538
|
{
|
|
1353
1539
|
iconName: this.runningCount ? 'spinner' : 'play',
|
|
1354
1540
|
label: 'Run',
|
|
@@ -1359,6 +1545,20 @@
|
|
|
1359
1545
|
},
|
|
1360
1546
|
].filter(btn => !btn.showIf || btn.showIf.call(this));
|
|
1361
1547
|
},
|
|
1548
|
+
/**
|
|
1549
|
+
* The history's entries (newest first) for the history list.
|
|
1550
|
+
*/
|
|
1551
|
+
historyEntries() {
|
|
1552
|
+
return this.commandHistory.map((code, index) => {
|
|
1553
|
+
const [group] = parseJSCodeGroups(code);
|
|
1554
|
+
const lines = (group?.lines ?? code).split('\n').filter(line => line.trim());
|
|
1555
|
+
return {
|
|
1556
|
+
index,
|
|
1557
|
+
label: group?.headerLines || lines[0] || '',
|
|
1558
|
+
detail: `${lines.length} line${lines.length === 1 ? '' : 's'}`,
|
|
1559
|
+
};
|
|
1560
|
+
}).reverse();
|
|
1561
|
+
},
|
|
1362
1562
|
canShrinkText() {
|
|
1363
1563
|
return this.textScale > TEXT_SCALES[0];
|
|
1364
1564
|
},
|
|
@@ -1399,9 +1599,110 @@
|
|
|
1399
1599
|
this.runGroup(jsCodeGroup0);
|
|
1400
1600
|
|
|
1401
1601
|
// Remove the code that was run from the editor.
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
this.
|
|
1602
|
+
const remainingCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
|
|
1603
|
+
|
|
1604
|
+
if (this.historyIndex !== null) {
|
|
1605
|
+
// Code from the history was run so the unrun code comes back. It
|
|
1606
|
+
// doesn't count as moving through the code so hidden blocks still
|
|
1607
|
+
// run in their places.
|
|
1608
|
+
this.jsCode = [remainingCode, this.historyDraft].filter(code => code.trim()).join('\n');
|
|
1609
|
+
this.historyIndex = this.historyDraft = null;
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
this.jsCode = remainingCode;
|
|
1613
|
+
this.runCount++;
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
/**
|
|
1617
|
+
* Shows older (-1) or newer (1) code from the history in the editor.
|
|
1618
|
+
* Going newer than the newest shows the unrun code again.
|
|
1619
|
+
* @param {-1|1} direction
|
|
1620
|
+
* @returns {boolean}
|
|
1621
|
+
* `true` if the history was used (so the key shouldn't do anything
|
|
1622
|
+
* else).
|
|
1623
|
+
*/
|
|
1624
|
+
navigateHistory(direction) {
|
|
1625
|
+
const history = this.commandHistory;
|
|
1626
|
+
if (direction < 0) {
|
|
1627
|
+
if (!history.length) return false;
|
|
1628
|
+
if (this.historyIndex === null) {
|
|
1629
|
+
this.historyDraft = this.jsCode;
|
|
1630
|
+
this.historyIndex = history.length;
|
|
1631
|
+
}
|
|
1632
|
+
if (this.historyIndex > 0) this.showHistoryCode(this.historyIndex - 1, 'start');
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
if (this.historyIndex === null) return false;
|
|
1636
|
+
if (this.historyIndex < history.length - 1) {
|
|
1637
|
+
this.showHistoryCode(this.historyIndex + 1, 'end');
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
const draft = this.historyDraft;
|
|
1641
|
+
this.historyIndex = this.historyDraft = null;
|
|
1642
|
+
this.setEditorCode(draft, 'end');
|
|
1643
|
+
}
|
|
1644
|
+
return true;
|
|
1645
|
+
},
|
|
1646
|
+
/**
|
|
1647
|
+
* Shows code from the history in the editor (saving the unrun code if
|
|
1648
|
+
* this is the first time).
|
|
1649
|
+
* @param {number} index
|
|
1650
|
+
* @param {"start"|"end"} cursorAt
|
|
1651
|
+
*/
|
|
1652
|
+
showHistoryCode(index, cursorAt) {
|
|
1653
|
+
if (this.historyIndex === null) this.historyDraft = this.jsCode;
|
|
1654
|
+
this.historyIndex = index;
|
|
1655
|
+
this.setEditorCode(this.commandHistory[index], cursorAt);
|
|
1656
|
+
},
|
|
1657
|
+
/**
|
|
1658
|
+
* @param {string} code
|
|
1659
|
+
* @param {"start"|"end"} cursorAt
|
|
1660
|
+
* Where the cursor goes so that pressing the same arrow key again
|
|
1661
|
+
* keeps going through the history.
|
|
1662
|
+
*/
|
|
1663
|
+
setEditorCode(code, cursorAt) {
|
|
1664
|
+
this.jsCode = code;
|
|
1665
|
+
this.$nextTick(() => {
|
|
1666
|
+
const editor = this.$refs.editor?.editor;
|
|
1667
|
+
if (!editor) return;
|
|
1668
|
+
if (cursorAt === 'end') editor.navigateFileEnd();
|
|
1669
|
+
else editor.navigateFileStart();
|
|
1670
|
+
});
|
|
1671
|
+
},
|
|
1672
|
+
/**
|
|
1673
|
+
* Called by the editor when the up or down arrow is pressed with the
|
|
1674
|
+
* cursor at the very start or end (and nothing selected).
|
|
1675
|
+
* @param {{direction: -1|1, handled: boolean}} event
|
|
1676
|
+
*/
|
|
1677
|
+
onEditorHistoryKey(event) {
|
|
1678
|
+
event.handled = this.navigateHistory(event.direction);
|
|
1679
|
+
},
|
|
1680
|
+
toggleHistory() {
|
|
1681
|
+
if (this.isHistoryOpen) {
|
|
1682
|
+
this.isHistoryOpen = false;
|
|
1683
|
+
}
|
|
1684
|
+
else {
|
|
1685
|
+
this.closeMenu();
|
|
1686
|
+
this.closeValueMenu();
|
|
1687
|
+
this.isHistoryOpen = true;
|
|
1688
|
+
this.$nextTick(() => this.$refs.historyPanel?.querySelector('.menu-item')?.focus());
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
/**
|
|
1692
|
+
* Puts code from the history list into the editor.
|
|
1693
|
+
* @param {number} index
|
|
1694
|
+
*/
|
|
1695
|
+
recallHistory(index) {
|
|
1696
|
+
this.isHistoryOpen = false;
|
|
1697
|
+
this.showHistoryCode(index, 'start');
|
|
1698
|
+
this.$nextTick(() => this.$refs.editor?.editor.focus());
|
|
1699
|
+
},
|
|
1700
|
+
/**
|
|
1701
|
+
* Forgets that the history was being gone through (eg. when the code in
|
|
1702
|
+
* the editor is replaced).
|
|
1703
|
+
*/
|
|
1704
|
+
stopNavigatingHistory() {
|
|
1705
|
+
this.historyIndex = this.historyDraft = null;
|
|
1405
1706
|
},
|
|
1406
1707
|
/**
|
|
1407
1708
|
* Runs the next hidden group if all of the visible code that came
|
|
@@ -1429,17 +1730,35 @@
|
|
|
1429
1730
|
});
|
|
1430
1731
|
this.runningCount++;
|
|
1431
1732
|
this.runHistory.push(group.allLines);
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
}
|
|
1733
|
+
// Hidden code wasn't typed or seen so it isn't in the history.
|
|
1734
|
+
const historyCode = group.allLines.trim();
|
|
1735
|
+
if (!isHidden && historyCode && this.commandHistory[this.commandHistory.length - 1] !== historyCode) {
|
|
1736
|
+
this.commandHistory.push(historyCode);
|
|
1737
|
+
if (this.commandHistory.length > 200) this.commandHistory.shift();
|
|
1738
|
+
}
|
|
1739
|
+
const {language, blockType} = this;
|
|
1740
|
+
const compiled = language === 'typescript'
|
|
1741
|
+
? compileTypeScript(group.lines, {blockType})
|
|
1742
|
+
: {code: group.lines};
|
|
1743
|
+
const {code, resultRange} = compiled.error
|
|
1744
|
+
? {code: '', resultRange: null}
|
|
1745
|
+
: prepareCode(compiled.code, {
|
|
1746
|
+
blockType,
|
|
1747
|
+
importsUrl: this.importsUrl,
|
|
1748
|
+
// Like the browser's console, show the value of the last
|
|
1749
|
+
// expression (except for hidden code).
|
|
1750
|
+
findResult: this.showResults && !isHidden,
|
|
1751
|
+
});
|
|
1439
1752
|
messageParent({
|
|
1440
1753
|
target: 'runner',
|
|
1441
1754
|
func: 'runCode',
|
|
1442
|
-
args: [code, {
|
|
1755
|
+
args: [code, {
|
|
1756
|
+
blockType,
|
|
1757
|
+
resultRange,
|
|
1758
|
+
language,
|
|
1759
|
+
sourceMap: compiled.sourceMap,
|
|
1760
|
+
compileError: compiled.error,
|
|
1761
|
+
}],
|
|
1443
1762
|
});
|
|
1444
1763
|
},
|
|
1445
1764
|
clearConsole() {
|
|
@@ -1470,6 +1789,7 @@
|
|
|
1470
1789
|
this.runCount = 0;
|
|
1471
1790
|
this.runningCount = 0;
|
|
1472
1791
|
this.runHistory = [];
|
|
1792
|
+
this.stopNavigatingHistory();
|
|
1473
1793
|
messageParent({target: 'runner', func: 'reset', args: []});
|
|
1474
1794
|
this.runHiddenGroups();
|
|
1475
1795
|
},
|
|
@@ -1512,14 +1832,17 @@
|
|
|
1512
1832
|
async saveFile() {
|
|
1513
1833
|
this.closeMenu();
|
|
1514
1834
|
const code = this.currentCode + '\n';
|
|
1515
|
-
const suggestedName = this.fileName || 'js-box.js';
|
|
1835
|
+
const suggestedName = this.fileName || (this.isTypeScript ? 'js-box.ts' : 'js-box.js');
|
|
1836
|
+
const fileType = this.isTypeScript
|
|
1837
|
+
? {description: 'TypeScript', accept: {'text/typescript': ['.ts', '.mts']}}
|
|
1838
|
+
: {description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}};
|
|
1516
1839
|
|
|
1517
1840
|
// Shows a "Save as" dialog where it is supported.
|
|
1518
1841
|
if ('function' === typeof window.showSaveFilePicker) {
|
|
1519
1842
|
try {
|
|
1520
1843
|
const handle = await showSaveFilePicker({
|
|
1521
1844
|
suggestedName,
|
|
1522
|
-
types: [
|
|
1845
|
+
types: [fileType],
|
|
1523
1846
|
});
|
|
1524
1847
|
const writable = await handle.createWritable();
|
|
1525
1848
|
await writable.write(code);
|
|
@@ -1533,7 +1856,7 @@
|
|
|
1533
1856
|
}
|
|
1534
1857
|
}
|
|
1535
1858
|
|
|
1536
|
-
const url = URL.createObjectURL(new Blob([code], {type:
|
|
1859
|
+
const url = URL.createObjectURL(new Blob([code], {type: Object.keys(fileType.accept)[0]}));
|
|
1537
1860
|
Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
|
|
1538
1861
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1539
1862
|
},
|
|
@@ -1552,6 +1875,7 @@
|
|
|
1552
1875
|
});
|
|
1553
1876
|
if (!isConfirmed) return;
|
|
1554
1877
|
}
|
|
1878
|
+
this.stopNavigatingHistory();
|
|
1555
1879
|
this.jsCode = code;
|
|
1556
1880
|
},
|
|
1557
1881
|
/**
|
|
@@ -1637,6 +1961,9 @@
|
|
|
1637
1961
|
jsCode: this.jsCode,
|
|
1638
1962
|
originalCode: this.originalCode,
|
|
1639
1963
|
fileName: this.fileName,
|
|
1964
|
+
commandHistory: this.commandHistory,
|
|
1965
|
+
historyIndex: this.historyIndex,
|
|
1966
|
+
historyDraft: this.historyDraft,
|
|
1640
1967
|
runHistory: this.runHistory,
|
|
1641
1968
|
hiddenGroups: this.hiddenGroups,
|
|
1642
1969
|
runCount: this.runCount,
|
|
@@ -1658,6 +1985,8 @@
|
|
|
1658
1985
|
this.closeMenu();
|
|
1659
1986
|
}
|
|
1660
1987
|
else {
|
|
1988
|
+
this.isHistoryOpen = false;
|
|
1989
|
+
this.closeValueMenu();
|
|
1661
1990
|
this.isMenuOpen = true;
|
|
1662
1991
|
this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
|
|
1663
1992
|
}
|
|
@@ -1670,14 +1999,21 @@
|
|
|
1670
1999
|
* Lets the arrow keys move between the menu's items.
|
|
1671
2000
|
* @param {KeyboardEvent} evt
|
|
1672
2001
|
*/
|
|
2002
|
+
/**
|
|
2003
|
+
* Lets Escape close a menu (or the history list) and the arrow keys move
|
|
2004
|
+
* between its items.
|
|
2005
|
+
* @param {KeyboardEvent} evt
|
|
2006
|
+
*/
|
|
1673
2007
|
onMenuKeyDown(evt) {
|
|
1674
2008
|
if (evt.key === 'Escape') {
|
|
1675
2009
|
evt.stopPropagation();
|
|
1676
|
-
this.closeMenu(
|
|
2010
|
+
this.closeMenu(this.isMenuOpen);
|
|
2011
|
+
this.closeValueMenu();
|
|
2012
|
+
this.isHistoryOpen = false;
|
|
1677
2013
|
}
|
|
1678
2014
|
else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
|
|
1679
2015
|
evt.preventDefault();
|
|
1680
|
-
const items = [...
|
|
2016
|
+
const items = [...evt.currentTarget.querySelectorAll('.menu-item:not(:disabled)')];
|
|
1681
2017
|
const index = items.indexOf(document.activeElement);
|
|
1682
2018
|
const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
|
|
1683
2019
|
next?.focus();
|
|
@@ -1757,15 +2093,189 @@
|
|
|
1757
2093
|
if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
|
|
1758
2094
|
this.closeMenu();
|
|
1759
2095
|
}
|
|
2096
|
+
if (this.valueMenu && !evt.target.closest('.value-menu')) {
|
|
2097
|
+
this.closeValueMenu();
|
|
2098
|
+
}
|
|
2099
|
+
if (this.isHistoryOpen && !evt.target.closest('.history-panel, .history-button')) {
|
|
2100
|
+
this.isHistoryOpen = false;
|
|
2101
|
+
}
|
|
1760
2102
|
},
|
|
1761
2103
|
/**
|
|
1762
2104
|
* @param {KeyboardEvent} evt
|
|
1763
2105
|
*/
|
|
1764
2106
|
onWindowKeyDown(evt) {
|
|
2107
|
+
if (evt.altKey && !evt.ctrlKey && !evt.metaKey && evt.code === 'KeyH') {
|
|
2108
|
+
evt.preventDefault();
|
|
2109
|
+
evt.stopPropagation();
|
|
2110
|
+
this.toggleHistory();
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (evt.key === 'Escape' && this.valueMenu) {
|
|
2114
|
+
this.closeValueMenu();
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
1765
2117
|
if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
|
|
1766
2118
|
this.setMaximized(false);
|
|
1767
2119
|
}
|
|
1768
2120
|
},
|
|
2121
|
+
/**
|
|
2122
|
+
* Shows the menu for a value that was right-clicked.
|
|
2123
|
+
* @param {MouseEvent} evt
|
|
2124
|
+
* @param {{path: any[], keyPath: string[]?, isPrimitive: boolean}} value
|
|
2125
|
+
*/
|
|
2126
|
+
openValueMenu(evt, {path, keyPath, isPrimitive}) {
|
|
2127
|
+
this.closeMenu();
|
|
2128
|
+
this.valueMenu = {
|
|
2129
|
+
x: evt.clientX,
|
|
2130
|
+
y: evt.clientY,
|
|
2131
|
+
path: path.slice(),
|
|
2132
|
+
// Only properties and items (not logged values themselves) have
|
|
2133
|
+
// a property path.
|
|
2134
|
+
propertyPath: keyPath?.length ? formatPropertyPath(keyPath) : null,
|
|
2135
|
+
fileName: keyPath?.length && /^[\w$-]+$/.test(keyPath[keyPath.length - 1]) ? keyPath[keyPath.length - 1] : 'value',
|
|
2136
|
+
isPrimitive,
|
|
2137
|
+
};
|
|
2138
|
+
// Keeps the menu inside of the window.
|
|
2139
|
+
this.$nextTick(() => {
|
|
2140
|
+
const menu = this.$refs.valueMenu;
|
|
2141
|
+
if (!menu || !this.valueMenu) return;
|
|
2142
|
+
const {width, height} = menu.getBoundingClientRect();
|
|
2143
|
+
this.valueMenu.x = Math.max(4, Math.min(this.valueMenu.x, innerWidth - width - 4));
|
|
2144
|
+
this.valueMenu.y = Math.max(4, Math.min(this.valueMenu.y, innerHeight - height - 4));
|
|
2145
|
+
menu.querySelector('.menu-item')?.focus();
|
|
2146
|
+
});
|
|
2147
|
+
},
|
|
2148
|
+
closeValueMenu() {
|
|
2149
|
+
this.valueMenu = null;
|
|
2150
|
+
},
|
|
2151
|
+
/**
|
|
2152
|
+
* Asks the runner to convert the value to JSON.
|
|
2153
|
+
* @param {any[]} path
|
|
2154
|
+
* @returns {Promise<{json: string?, circularCount: number, error: string?}>}
|
|
2155
|
+
*/
|
|
2156
|
+
requestValueJson(path) {
|
|
2157
|
+
const requestId = `${Date.now()}-${Math.random()}`;
|
|
2158
|
+
return new Promise(resolve => {
|
|
2159
|
+
jsonRequests.set(requestId, resolve);
|
|
2160
|
+
messageParent({target: 'runner', func: 'getValueAsJson', args: [path, requestId]});
|
|
2161
|
+
});
|
|
2162
|
+
},
|
|
2163
|
+
/**
|
|
2164
|
+
* Shows a short message (eg. "Copied") for a few seconds.
|
|
2165
|
+
* @param {string} message
|
|
2166
|
+
*/
|
|
2167
|
+
showToast(message) {
|
|
2168
|
+
clearTimeout(this.toastTimeout);
|
|
2169
|
+
this.toast = {message};
|
|
2170
|
+
this.toastTimeout = setTimeout(() => this.toast = null, 3500);
|
|
2171
|
+
},
|
|
2172
|
+
/**
|
|
2173
|
+
* @param {{json: string?, circularCount: number, error: string?}} result
|
|
2174
|
+
* @param {string} action
|
|
2175
|
+
* What was done (eg. "Copied as JSON").
|
|
2176
|
+
*/
|
|
2177
|
+
showJsonToast({circularCount, error}, action) {
|
|
2178
|
+
this.showToast(
|
|
2179
|
+
error || action + (circularCount
|
|
2180
|
+
? ` (${circularCount} circular reference${circularCount === 1 ? ' was' : 's were'} left out)`
|
|
2181
|
+
: '')
|
|
2182
|
+
);
|
|
2183
|
+
},
|
|
2184
|
+
/**
|
|
2185
|
+
* Copies the value as JSON. The copy is started right away (while
|
|
2186
|
+
* the browser still allows it) with the JSON filled in when it is
|
|
2187
|
+
* ready.
|
|
2188
|
+
*/
|
|
2189
|
+
async copyValueAsJson() {
|
|
2190
|
+
// A plain copy because the menu's (reactive) path can't be sent.
|
|
2191
|
+
const path = [...this.valueMenu.path];
|
|
2192
|
+
this.closeValueMenu();
|
|
2193
|
+
const resultPromise = this.requestValueJson(path);
|
|
2194
|
+
try {
|
|
2195
|
+
if (window.ClipboardItem && navigator.clipboard?.write) {
|
|
2196
|
+
await navigator.clipboard.write([new ClipboardItem({
|
|
2197
|
+
'text/plain': resultPromise.then(({json, error}) => {
|
|
2198
|
+
if (error) throw new Error(error);
|
|
2199
|
+
return new Blob([json], {type: 'text/plain'});
|
|
2200
|
+
}),
|
|
2201
|
+
})]);
|
|
2202
|
+
}
|
|
2203
|
+
else {
|
|
2204
|
+
const {json, error} = await resultPromise;
|
|
2205
|
+
if (error) throw new Error(error);
|
|
2206
|
+
await navigator.clipboard.writeText(json);
|
|
2207
|
+
}
|
|
2208
|
+
this.showJsonToast(await resultPromise, 'Copied as JSON');
|
|
2209
|
+
}
|
|
2210
|
+
catch (e) {
|
|
2211
|
+
const result = await resultPromise;
|
|
2212
|
+
this.showToast(result.error || 'The value couldn\'t be copied. Your browser may not allow copying here.');
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
/**
|
|
2216
|
+
* Saves the value as a JSON file. The "Save as" dialog is shown right
|
|
2217
|
+
* away (while the browser still allows it).
|
|
2218
|
+
*/
|
|
2219
|
+
async saveValueAsJson() {
|
|
2220
|
+
const {fileName} = this.valueMenu;
|
|
2221
|
+
const path = [...this.valueMenu.path];
|
|
2222
|
+
this.closeValueMenu();
|
|
2223
|
+
const resultPromise = this.requestValueJson(path);
|
|
2224
|
+
const suggestedName = `${fileName}.json`;
|
|
2225
|
+
let handle;
|
|
2226
|
+
if ('function' === typeof window.showSaveFilePicker) {
|
|
2227
|
+
try {
|
|
2228
|
+
handle = await showSaveFilePicker({
|
|
2229
|
+
suggestedName,
|
|
2230
|
+
types: [{description: 'JSON', accept: {'application/json': ['.json']}}],
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
catch (e) {
|
|
2234
|
+
if (e?.name === 'AbortError') return;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const result = await resultPromise;
|
|
2238
|
+
if (result.error) {
|
|
2239
|
+
this.showToast(result.error);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
const text = result.json + '\n';
|
|
2243
|
+
if (handle) {
|
|
2244
|
+
const writable = await handle.createWritable();
|
|
2245
|
+
await writable.write(text);
|
|
2246
|
+
await writable.close();
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
const url = URL.createObjectURL(new Blob([text], {type: 'application/json'}));
|
|
2250
|
+
Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
|
|
2251
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
2252
|
+
}
|
|
2253
|
+
this.showJsonToast(result, 'Saved as JSON');
|
|
2254
|
+
},
|
|
2255
|
+
storeValueAsGlobal() {
|
|
2256
|
+
const path = [...this.valueMenu.path];
|
|
2257
|
+
this.closeValueMenu();
|
|
2258
|
+
messageParent({target: 'runner', func: 'storeAsGlobal', args: [path]});
|
|
2259
|
+
},
|
|
2260
|
+
async copyPropertyPath() {
|
|
2261
|
+
const {propertyPath} = this.valueMenu;
|
|
2262
|
+
this.closeValueMenu();
|
|
2263
|
+
try {
|
|
2264
|
+
await navigator.clipboard.writeText(propertyPath);
|
|
2265
|
+
this.showToast(`Copied ${propertyPath}`);
|
|
2266
|
+
}
|
|
2267
|
+
catch (e) {
|
|
2268
|
+
this.showToast('The property path couldn\'t be copied. Your browser may not allow copying here.');
|
|
2269
|
+
}
|
|
2270
|
+
},
|
|
2271
|
+
/**
|
|
2272
|
+
* Shows the value as it is now (eg. after the code changed it).
|
|
2273
|
+
*/
|
|
2274
|
+
refreshValue() {
|
|
2275
|
+
const path = [...this.valueMenu.path];
|
|
2276
|
+
this.closeValueMenu();
|
|
2277
|
+
messageParent({target: 'runner', func: 'refreshDescription', args: [path]});
|
|
2278
|
+
},
|
|
1769
2279
|
closeDialog(isConfirmed) {
|
|
1770
2280
|
const {dialog} = this;
|
|
1771
2281
|
this.dialog = null;
|
|
@@ -1786,6 +2296,7 @@
|
|
|
1786
2296
|
}
|
|
1787
2297
|
},
|
|
1788
2298
|
onDisplaysScroll() {
|
|
2299
|
+
this.closeValueMenu();
|
|
1789
2300
|
const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
|
|
1790
2301
|
this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
|
|
1791
2302
|
},
|
|
@@ -1859,7 +2370,8 @@
|
|
|
1859
2370
|
mounted() {
|
|
1860
2371
|
addEventListener('mousemove', this.onWindowMouseMove);
|
|
1861
2372
|
addEventListener('mousedown', this.onWindowMouseDown);
|
|
1862
|
-
|
|
2373
|
+
// Capturing lets shortcuts (eg. Alt+H) work before the editor sees them.
|
|
2374
|
+
addEventListener('keydown', this.onWindowKeyDown, true);
|
|
1863
2375
|
addEventListener('resize', this.onWindowResize);
|
|
1864
2376
|
addEventListener('mouseup', this.onWindowMouseUp);
|
|
1865
2377
|
addEventListener('error', this.onWindowError);
|
|
@@ -2005,6 +2517,28 @@
|
|
|
2005
2517
|
});
|
|
2006
2518
|
|
|
2007
2519
|
// Attempt to capture key combos.
|
|
2520
|
+
// Like the browser's console, the up arrow at the very start (or the
|
|
2521
|
+
// down arrow at the very end) with nothing selected goes through the
|
|
2522
|
+
// history. This listens while capturing so it runs before Ace moves the
|
|
2523
|
+
// cursor.
|
|
2524
|
+
this.$refs.editor.addEventListener('keydown', evt => {
|
|
2525
|
+
const isUp = evt.key === 'ArrowUp';
|
|
2526
|
+
if (!isUp && evt.key !== 'ArrowDown') return;
|
|
2527
|
+
if (evt.shiftKey || evt.altKey || evt.ctrlKey || evt.metaKey || !editor.selection.isEmpty()) return;
|
|
2528
|
+
const {row, column} = editor.getCursorPosition();
|
|
2529
|
+
const lastRow = editor.session.getLength() - 1;
|
|
2530
|
+
const isAtEdge = isUp
|
|
2531
|
+
? row === 0 && column === 0
|
|
2532
|
+
: row === lastRow && column === editor.session.getLine(lastRow).length;
|
|
2533
|
+
if (!isAtEdge) return;
|
|
2534
|
+
const event = {direction: isUp ? -1 : 1, handled: false};
|
|
2535
|
+
this.$emit('historyKey', event);
|
|
2536
|
+
if (event.handled) {
|
|
2537
|
+
evt.preventDefault();
|
|
2538
|
+
evt.stopPropagation();
|
|
2539
|
+
}
|
|
2540
|
+
}, true);
|
|
2541
|
+
|
|
2008
2542
|
editor.textInput.getElement().addEventListener('keydown', (evt) => {
|
|
2009
2543
|
const isComboKeyCode = evt.keyCode === 16 // SHIFT
|
|
2010
2544
|
|| evt.keyCode === 17 // CTRL
|
|
@@ -2064,7 +2598,7 @@
|
|
|
2064
2598
|
|
|
2065
2599
|
codeElem.textContent = this.code;
|
|
2066
2600
|
|
|
2067
|
-
codeElem.className = `language
|
|
2601
|
+
codeElem.className = `language-${this.language ?? 'javascript'}`;
|
|
2068
2602
|
preElem.className = '';
|
|
2069
2603
|
|
|
2070
2604
|
for (const propName of ['lineNumbers', 'matchBraces']) {
|
|
@@ -2109,6 +2643,10 @@
|
|
|
2109
2643
|
// https://icon-sets.iconify.design/mdi/error-outline/
|
|
2110
2644
|
error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
|
|
2111
2645
|
// Icons similar to those in the browser's console.
|
|
2646
|
+
history: '<svg viewBox="0 0 16 16"><path d="M2.5 8a5.5 5.5 0 1 0 1.6-3.9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M2.25 2.5v2.75H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 5v3.25l2.25 1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
2647
|
+
copy: '<svg viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 3.5v-.75A1.25 1.25 0 0 0 9.25 1.5h-5A1.25 1.25 0 0 0 3 2.75v5A1.25 1.25 0 0 0 4.25 9h.75" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
|
|
2648
|
+
variable: '<svg viewBox="0 0 16 16"><path d="M5 2.5c-1.5 1.5-2 3.5-2 5.5s.5 4 2 5.5M11 2.5c1.5 1.5 2 3.5 2 5.5s-.5 4-2 5.5M6.5 6l3 4M9.5 6l-3 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
|
|
2649
|
+
path: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="12.5" r="1.5" fill="currentColor"/><circle cx="12.5" cy="3.5" r="1.5" fill="currentColor"/><path d="M3.5 11V8a2 2 0 0 1 2-2h5a2 2 0 0 0 2-2" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
|
|
2112
2650
|
open: '<svg viewBox="0 0 16 16"><path d="M2 4.5a1 1 0 0 1 1-1h3.2l1.3 1.5H13a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>',
|
|
2113
2651
|
save: '<svg viewBox="0 0 16 16"><path d="M8 2.5v7M5 7l3 3 3-3M3 11.5v1.5h10v-1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
2114
2652
|
fullscreen: '<svg viewBox="0 0 16 16"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
@@ -2136,7 +2674,10 @@
|
|
|
2136
2674
|
|
|
2137
2675
|
function getJSValueComponentProps() {
|
|
2138
2676
|
return {
|
|
2139
|
-
|
|
2677
|
+
// keyPath is the property keys leading to this value from the logged
|
|
2678
|
+
// value (eg. ["people", "0", "name"]) or null if it can't be reached by
|
|
2679
|
+
// property keys (eg. inside of a Map).
|
|
2680
|
+
props: ['description', 'path', 'name', 'isDimName', 'keyPath'],
|
|
2140
2681
|
data() {
|
|
2141
2682
|
return {
|
|
2142
2683
|
isExpanded: false,
|
|
@@ -2148,7 +2689,12 @@
|
|
|
2148
2689
|
if (newValue && !this.hasBeenExpanded) {
|
|
2149
2690
|
this.hasBeenExpanded = newValue;
|
|
2150
2691
|
}
|
|
2151
|
-
}
|
|
2692
|
+
},
|
|
2693
|
+
// When a refresh replaces this value's description, its new contents are
|
|
2694
|
+
// requested if it is still expanded.
|
|
2695
|
+
isPartialDescription(isPartial) {
|
|
2696
|
+
if (isPartial && this.isExpanded) this.requestDescription();
|
|
2697
|
+
},
|
|
2152
2698
|
},
|
|
2153
2699
|
computed: {
|
|
2154
2700
|
isPartialDescription() {
|
|
@@ -2169,10 +2715,20 @@
|
|
|
2169
2715
|
];
|
|
2170
2716
|
},
|
|
2171
2717
|
entryGroups() {
|
|
2172
|
-
// protoEntries only ever contains the [[Prototype]] entry
|
|
2718
|
+
// protoEntries only ever contains the [[Prototype]] entry whose
|
|
2719
|
+
// members can be reached directly so it adds nothing to the key path.
|
|
2720
|
+
const {keyPath, description} = this;
|
|
2173
2721
|
return [
|
|
2174
|
-
{
|
|
2175
|
-
|
|
2722
|
+
{
|
|
2723
|
+
key: 'entries',
|
|
2724
|
+
isDim: entry => entry[2] === false,
|
|
2725
|
+
getKeyPath: entry => keyPath && description.entriesArePropertyKeys ? keyPath.concat([entry[0]]) : null,
|
|
2726
|
+
},
|
|
2727
|
+
{
|
|
2728
|
+
key: 'protoEntries',
|
|
2729
|
+
isDim: () => true,
|
|
2730
|
+
getKeyPath: () => keyPath,
|
|
2731
|
+
},
|
|
2176
2732
|
];
|
|
2177
2733
|
}
|
|
2178
2734
|
},
|
|
@@ -2180,14 +2736,26 @@
|
|
|
2180
2736
|
toggleExpanded() {
|
|
2181
2737
|
if (!this.isExpandable) return;
|
|
2182
2738
|
this.isExpanded = !this.isExpanded;
|
|
2183
|
-
if (this.isExpanded && this.isPartialDescription)
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2739
|
+
if (this.isExpanded && this.isPartialDescription) this.requestDescription();
|
|
2740
|
+
},
|
|
2741
|
+
requestDescription() {
|
|
2742
|
+
messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
|
|
2743
|
+
},
|
|
2744
|
+
/**
|
|
2745
|
+
* Shows the menu for this value (eg. Copy as JSON).
|
|
2746
|
+
* @param {MouseEvent} evt
|
|
2747
|
+
*/
|
|
2748
|
+
openMenu(evt) {
|
|
2749
|
+
this.$root.openValueMenu(evt, {
|
|
2750
|
+
path: this.path,
|
|
2751
|
+
keyPath: this.keyPath,
|
|
2752
|
+
isPrimitive: this.description.isPrimitive,
|
|
2753
|
+
});
|
|
2754
|
+
},
|
|
2187
2755
|
},
|
|
2188
2756
|
template: `
|
|
2189
2757
|
<div :class="classNames">
|
|
2190
|
-
<div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
|
|
2758
|
+
<div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded" @contextmenu.prevent.stop="openMenu"><span
|
|
2191
2759
|
v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
|
|
2192
2760
|
v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
|
|
2193
2761
|
v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
|
|
@@ -2200,6 +2768,7 @@
|
|
|
2200
2768
|
:name="entry[0]"
|
|
2201
2769
|
:is-dim-name="group.isDim(entry)"
|
|
2202
2770
|
:description="entry[1]"
|
|
2771
|
+
:key-path="group.getKeyPath(entry)"
|
|
2203
2772
|
:path="path.concat([group.key, entryIndex])">
|
|
2204
2773
|
</js-value>
|
|
2205
2774
|
</template>
|
|
@@ -2302,6 +2871,88 @@
|
|
|
2302
2871
|
};
|
|
2303
2872
|
}
|
|
2304
2873
|
|
|
2874
|
+
/**
|
|
2875
|
+
* Compiles TypeScript to JavaScript by removing the types (without checking
|
|
2876
|
+
* them) using Babel. Every line stays on the same line so that line numbers
|
|
2877
|
+
* in errors still match.
|
|
2878
|
+
* @param {string} code
|
|
2879
|
+
* @param {Object} options
|
|
2880
|
+
* @param {"classic"|"module"} options.blockType
|
|
2881
|
+
* @returns {{code: string, sourceMap?: number[][][], error?: {message: string, line: number, column: number}}}
|
|
2882
|
+
* The JavaScript code and, for each of its lines, where it came from (see
|
|
2883
|
+
* decodeSourceMap()) or the syntax error that kept it from being compiled.
|
|
2884
|
+
*/
|
|
2885
|
+
function compileTypeScript(code, {blockType}) {
|
|
2886
|
+
try {
|
|
2887
|
+
const result = Babel.transform(code, {
|
|
2888
|
+
filename: 'snippet.ts',
|
|
2889
|
+
presets: [['typescript', {
|
|
2890
|
+
allExtensions: true,
|
|
2891
|
+
// Only `import type` is removed so that an import is never dropped
|
|
2892
|
+
// just because it isn't used yet.
|
|
2893
|
+
onlyRemoveTypeImports: true,
|
|
2894
|
+
}]],
|
|
2895
|
+
sourceType: blockType === 'module' ? 'module' : 'script',
|
|
2896
|
+
// Allows `export` in a namespace in classic blocks. Imports in classic
|
|
2897
|
+
// blocks are left in so that running them shows the usual error.
|
|
2898
|
+
parserOpts: {allowImportExportEverywhere: blockType !== 'module'},
|
|
2899
|
+
retainLines: true,
|
|
2900
|
+
sourceMaps: true,
|
|
2901
|
+
});
|
|
2902
|
+
return {code: result.code, sourceMap: decodeSourceMap(result.map.mappings)};
|
|
2903
|
+
}
|
|
2904
|
+
catch (e) {
|
|
2905
|
+
if (e?.name !== 'SyntaxError' || !e.loc) throw e;
|
|
2906
|
+
return {
|
|
2907
|
+
code,
|
|
2908
|
+
error: {
|
|
2909
|
+
// Babel's message starts with the file name and ends with the
|
|
2910
|
+
// position and the code where the error is.
|
|
2911
|
+
message: `${e.message}`.split('\n')[0].replace(/^\/?snippet\.ts:\s*/, '').replace(/\s*\(\d+:\d+\)$/, ''),
|
|
2912
|
+
line: e.loc.line,
|
|
2913
|
+
column: e.loc.column + 1,
|
|
2914
|
+
},
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
/**
|
|
2920
|
+
* Decodes the mappings of a source map (for a single source).
|
|
2921
|
+
* @param {string} mappings
|
|
2922
|
+
* @returns {number[][][]}
|
|
2923
|
+
* For each line of the compiled code, its mappings as [compiled column,
|
|
2924
|
+
* original line, original column] (all starting at 0) sorted by column.
|
|
2925
|
+
*/
|
|
2926
|
+
function decodeSourceMap(mappings) {
|
|
2927
|
+
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
2928
|
+
const totals = [0, 0, 0, 0, 0];
|
|
2929
|
+
return mappings.split(';').map(line => {
|
|
2930
|
+
totals[0] = 0;
|
|
2931
|
+
const lineMappings = [];
|
|
2932
|
+
for (const segment of line.split(',')) {
|
|
2933
|
+
if (!segment) continue;
|
|
2934
|
+
const fields = [];
|
|
2935
|
+
let value = 0, shift = 0;
|
|
2936
|
+
for (const char of segment) {
|
|
2937
|
+
const digit = BASE64.indexOf(char);
|
|
2938
|
+
value += (digit & 31) << shift;
|
|
2939
|
+
if (digit & 32) {
|
|
2940
|
+
shift += 5;
|
|
2941
|
+
}
|
|
2942
|
+
else {
|
|
2943
|
+
fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
2944
|
+
value = shift = 0;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
// The fields are the compiled column, the source index, the original
|
|
2948
|
+
// line and the original column, each relative to the previous one.
|
|
2949
|
+
fields.forEach((field, index) => totals[index] += field);
|
|
2950
|
+
if (fields.length >= 4) lineMappings.push([totals[0], totals[2], totals[3]]);
|
|
2951
|
+
}
|
|
2952
|
+
return lineMappings.sort((a, b) => a[0] - b[0]);
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2305
2956
|
/**
|
|
2306
2957
|
* Gets a block of code ready to run: packages imported by name (eg.
|
|
2307
2958
|
* `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
|
|
@@ -2367,6 +3018,22 @@
|
|
|
2367
3018
|
return {code, resultRange};
|
|
2368
3019
|
}
|
|
2369
3020
|
|
|
3021
|
+
/**
|
|
3022
|
+
* Turns property keys into a path like the browser's console's "Copy property
|
|
3023
|
+
* path" (eg. ["people", "0", "first name"] becomes `people[0]["first name"]`).
|
|
3024
|
+
* @param {string[]} keys
|
|
3025
|
+
* @returns {string}
|
|
3026
|
+
*/
|
|
3027
|
+
function formatPropertyPath(keys) {
|
|
3028
|
+
return keys.map((key, index) =>
|
|
3029
|
+
/^(0|[1-9]\d*)$/.test(key)
|
|
3030
|
+
? `[${key}]`
|
|
3031
|
+
: /^[A-Za-z_$][\w$]*$/.test(key)
|
|
3032
|
+
? (index ? '.' : '') + key
|
|
3033
|
+
: `[${JSON.stringify(key)}]`
|
|
3034
|
+
).join('');
|
|
3035
|
+
}
|
|
3036
|
+
|
|
2370
3037
|
/**
|
|
2371
3038
|
* Indicates if an import specifier is a package name (eg. "lodash",
|
|
2372
3039
|
* "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
|
|
@@ -2525,6 +3192,7 @@
|
|
|
2525
3192
|
`${Object(part)[1]}`,
|
|
2526
3193
|
]),
|
|
2527
3194
|
...sanitizeEntries(description),
|
|
3195
|
+
...(description.entriesArePropertyKeys != null ? {entriesArePropertyKeys: !!description.entriesArePropertyKeys} : {}),
|
|
2528
3196
|
};
|
|
2529
3197
|
}
|
|
2530
3198
|
|
|
@@ -2615,7 +3283,47 @@
|
|
|
2615
3283
|
level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
|
|
2616
3284
|
}
|
|
2617
3285
|
|
|
2618
|
-
if (level)
|
|
3286
|
+
if (!level) return;
|
|
3287
|
+
Object.assign(level, sanitizeEntries(description));
|
|
3288
|
+
level.entriesArePropertyKeys = !!Object(description).entriesArePropertyKeys;
|
|
3289
|
+
// A refresh also sends the value's preview.
|
|
3290
|
+
if (Array.isArray(Object(description).parts)) {
|
|
3291
|
+
const {typeName, isPrimitive, parts} = sanitizeDescription(description);
|
|
3292
|
+
Object.assign(level, {typeName, isPrimitive, parts});
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* NOTE: Called via main by the runner with a value converted to JSON (see
|
|
3298
|
+
* requestValueJson()).
|
|
3299
|
+
* @param {{requestId: string, json?: string, circularCount?: number, error?: string}} result
|
|
3300
|
+
*/
|
|
3301
|
+
function onValueJson(result) {
|
|
3302
|
+
const {requestId, json, circularCount, error} = Object(result);
|
|
3303
|
+
const resolve = jsonRequests.get(`${requestId}`);
|
|
3304
|
+
if (!resolve) return;
|
|
3305
|
+
jsonRequests.delete(`${requestId}`);
|
|
3306
|
+
resolve({
|
|
3307
|
+
json: json != null ? `${json}` : null,
|
|
3308
|
+
circularCount: +circularCount || 0,
|
|
3309
|
+
error: error != null ? `${error}` : null,
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
/**
|
|
3314
|
+
* The functions waiting for values to be converted to JSON by their request
|
|
3315
|
+
* IDs.
|
|
3316
|
+
* @type {Map<string, (result: {json: string?, circularCount: number, error: string?}) => void>}
|
|
3317
|
+
*/
|
|
3318
|
+
const jsonRequests = new Map();
|
|
3319
|
+
|
|
3320
|
+
/**
|
|
3321
|
+
* NOTE: Called via main by the runner after a value was stored as a global
|
|
3322
|
+
* variable.
|
|
3323
|
+
* @param {string} name
|
|
3324
|
+
*/
|
|
3325
|
+
function onStoredAsGlobal(name) {
|
|
3326
|
+
mountedApp.displays.push({type: 'notice', message: `Stored as the global variable ${`${name}`}`});
|
|
2619
3327
|
}
|
|
2620
3328
|
|
|
2621
3329
|
/**
|
|
@@ -2685,6 +3393,8 @@
|
|
|
2685
3393
|
// Used to find the last expression in each block of code (so that its
|
|
2686
3394
|
// value can be shown) and the packages that it imports.
|
|
2687
3395
|
libraryUrl('acorn', 'dist/acorn.js'),
|
|
3396
|
+
// Removes the types from TypeScript code before it is run.
|
|
3397
|
+
...language === 'typescript' ? [libraryUrl('@babel/standalone', 'babel.min.js')] : [],
|
|
2688
3398
|
],
|
|
2689
3399
|
cssUrls: [
|
|
2690
3400
|
'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
|
|
@@ -2703,6 +3413,7 @@
|
|
|
2703
3413
|
this.call('init', code, dataset, {
|
|
2704
3414
|
runnerMode,
|
|
2705
3415
|
blockType,
|
|
3416
|
+
language,
|
|
2706
3417
|
showResults,
|
|
2707
3418
|
importsUrl,
|
|
2708
3419
|
packageInfo: PACKAGE_INFO,
|
|
@@ -2779,7 +3490,7 @@
|
|
|
2779
3490
|
* The options of YourJSBox.create() which are the same as the data
|
|
2780
3491
|
* attributes of a script tag.
|
|
2781
3492
|
*/
|
|
2782
|
-
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
3493
|
+
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'language', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
2783
3494
|
|
|
2784
3495
|
/**
|
|
2785
3496
|
* The JavaScript API for creating consoles (available as window.YourJSBox).
|
|
@@ -2805,6 +3516,7 @@
|
|
|
2805
3516
|
* Optional. The code that the console starts with.
|
|
2806
3517
|
* @param {string=} options.runner
|
|
2807
3518
|
* @param {string=} options.blockType
|
|
3519
|
+
* @param {string=} options.language
|
|
2808
3520
|
* @param {(boolean|string)=} options.showResults
|
|
2809
3521
|
* @param {string=} options.hidePrefix
|
|
2810
3522
|
* @param {string=} options.dividerOrient
|