yourjs-box 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +30 -4
- package/dist/yourjs-box.full.js +581 -43
- package/dist/yourjs-box.js +8 -8
- package/dist/yourjs-box.min.js +6 -6
- package/package.json +2 -2
package/dist/yourjs-box.full.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/*! yourjs-box v1.
|
|
1
|
+
/*! yourjs-box v1.6.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
|
|
2
2
|
(() => {
|
|
3
3
|
/**
|
|
4
4
|
* Viewer IFRAME's CSS code
|
|
5
5
|
* @type {string}
|
|
6
6
|
*/
|
|
7
|
-
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
7
|
+
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.history-panel{max-width:min(420px,calc(100% - 12px));width:360px}.history-title{color:var(--muted-text);font-size:11px;font-weight:600;letter-spacing:.05em;padding:4px 8px 6px;text-transform:uppercase}.history-item{justify-content:space-between}.history-label{font-family:var(--console-font);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-detail{color:var(--muted-text);flex-shrink:0;font-size:11px}.history-empty,.history-hint{color:var(--muted-text);font-size:12px;padding:6px 8px}.history-hint{border-top:1px solid var(--toolbar-border);margin-top:4px}.value-menu{bottom:auto;min-width:220px;position:fixed;right:auto;z-index:850}.toast{animation:fade-in .15s ease-out;background-color:var(--console-text);border-radius:6px;bottom:44px;box-shadow:0 6px 20px rgb(0 0 0 / .25);color:var(--console-bg);font-size:12px;left:50%;max-width:calc(100% - 32px);padding:6px 12px;pointer-events:none;position:fixed;transform:translateX(-50%);z-index:860}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
8
8
|
/**
|
|
9
9
|
* Information about this package (eg. its version).
|
|
10
10
|
* @type {{name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}
|
|
11
11
|
*/
|
|
12
|
-
const PACKAGE_INFO = {"name":"yourjs-box","version":"1.
|
|
12
|
+
const PACKAGE_INFO = {"name":"yourjs-box","version":"1.6.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
|
|
13
13
|
/**
|
|
14
14
|
* Viewer IFRAME's HTML code
|
|
15
15
|
* @type {string}
|
|
16
16
|
*/
|
|
17
|
-
const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading…</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,.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\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf && 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>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 Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
|
|
18
18
|
/**
|
|
19
19
|
* The URL of this script which is used to remove this script's lines from the
|
|
20
20
|
* stack traces of errors in window mode.
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
* to these calls.
|
|
90
90
|
*/
|
|
91
91
|
const RELAYABLE_FUNCS = {
|
|
92
|
-
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
|
|
93
|
-
runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
|
|
92
|
+
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'onStoredAsGlobal', 'onValueJson', 'updateDescriptionFor'],
|
|
93
|
+
runner: ['clearLogs', 'getValueAsJson', 'refreshDescription', 'reset', 'runCode', 'sendDescriptionFor', 'storeAsGlobal'],
|
|
94
94
|
// Things that the viewer (but never the runner) can ask this script to do.
|
|
95
95
|
host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
|
|
96
96
|
};
|
|
@@ -631,9 +631,9 @@
|
|
|
631
631
|
const $entries = [];
|
|
632
632
|
const protoEntries = [];
|
|
633
633
|
const $protoEntries = [];
|
|
634
|
+
let wasIterated = false;
|
|
634
635
|
|
|
635
636
|
if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
|
|
636
|
-
let wasIterated = false;
|
|
637
637
|
try {
|
|
638
638
|
// Map
|
|
639
639
|
if (typeName === 'Map') {
|
|
@@ -681,7 +681,9 @@
|
|
|
681
681
|
}
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
-
|
|
684
|
+
// Map and Set entries (and other iterated values) aren't properties so
|
|
685
|
+
// they have no property path.
|
|
686
|
+
return {entries, protoEntries, entriesArePropertyKeys: !wasIterated, $entries, $protoEntries};
|
|
685
687
|
}
|
|
686
688
|
|
|
687
689
|
// ['123', 0, 'entries', 0]
|
|
@@ -792,16 +794,25 @@
|
|
|
792
794
|
* entry indices.
|
|
793
795
|
*/
|
|
794
796
|
function sendDescriptionFor(path) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
797
|
+
sendDescription(path, false);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Describes the value again (eg. after the code changed it) including its
|
|
802
|
+
* preview.
|
|
803
|
+
* @param {(string|number)[]} path
|
|
804
|
+
*/
|
|
805
|
+
function refreshDescription(path) {
|
|
806
|
+
sendDescription(path, true);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* @param {(string|number)[]} path
|
|
811
|
+
* @param {boolean} isRefresh
|
|
812
|
+
* If `true` the value's preview is included too.
|
|
813
|
+
*/
|
|
814
|
+
function sendDescription(path, isRefresh) {
|
|
815
|
+
const level = getLevel(path);
|
|
805
816
|
if (!level) return;
|
|
806
817
|
const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
|
|
807
818
|
|
|
@@ -809,11 +820,95 @@
|
|
|
809
820
|
Object.assign(level, description);
|
|
810
821
|
|
|
811
822
|
// Send the description without values back to the viewer.
|
|
823
|
+
const summary = isRefresh
|
|
824
|
+
? summarize(level.value, path.length > 2 ? 1 : 0, path[path.length - 2] === 'protoEntries')
|
|
825
|
+
: {};
|
|
812
826
|
send({
|
|
813
827
|
target: 'viewer',
|
|
814
828
|
func: 'updateDescriptionFor',
|
|
815
|
-
args: [path, without(['$entries', '$protoEntries'], description)],
|
|
829
|
+
args: [path, {...without(['$entries', '$protoEntries'], description), ...summary}],
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Finds what is stored for a logged value (or a value within it).
|
|
835
|
+
* @param {(string|number)[]} path
|
|
836
|
+
* The log ID, the argument index and then pairs of entry group keys and
|
|
837
|
+
* entry indices.
|
|
838
|
+
* @returns {{value: *, receiver?: *}|undefined}
|
|
839
|
+
*/
|
|
840
|
+
function getLevel(path) {
|
|
841
|
+
let level = logArgsById;
|
|
842
|
+
path.forEach((pathPart, index) => {
|
|
843
|
+
level = level?.[index && index % 2 === 0 ? '$' + pathPart : pathPart];
|
|
816
844
|
});
|
|
845
|
+
return level;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Sends a value as JSON to the viewer (eg. to copy or save it).
|
|
850
|
+
* @param {(string|number)[]} path
|
|
851
|
+
* @param {string} requestId
|
|
852
|
+
*/
|
|
853
|
+
function getValueAsJson(path, requestId) {
|
|
854
|
+
const level = getLevel(path);
|
|
855
|
+
let result;
|
|
856
|
+
try {
|
|
857
|
+
result = level ? toJson(level.value) : {error: 'The value is no longer available.'};
|
|
858
|
+
}
|
|
859
|
+
catch (e) {
|
|
860
|
+
result = {error: `The value couldn't be converted to JSON (${e?.message ?? e}).`};
|
|
861
|
+
}
|
|
862
|
+
send({target: 'viewer', func: 'onValueJson', args: [{requestId, ...result}]});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Converts a value to JSON like JSON.stringify() does except that Maps and
|
|
867
|
+
* Sets become arrays, BigInts become strings and circular references (an
|
|
868
|
+
* object inside of itself) are left out instead of causing an error. An
|
|
869
|
+
* object that appears more than once without being circular is kept.
|
|
870
|
+
* @param {*} value
|
|
871
|
+
* @returns {{json?: string, circularCount: number, error?: string}}
|
|
872
|
+
*/
|
|
873
|
+
function toJson(value) {
|
|
874
|
+
let circularCount = 0;
|
|
875
|
+
// The objects currently being converted along with what they were
|
|
876
|
+
// converted from (eg. a Map is converted to an array).
|
|
877
|
+
const stack = [];
|
|
878
|
+
const json = JSON.stringify(value, function(key, val) {
|
|
879
|
+
// `this` is the object containing `key` so anything after it on the
|
|
880
|
+
// stack has already been converted.
|
|
881
|
+
while (stack.length && stack[stack.length - 1].converted !== this) stack.pop();
|
|
882
|
+
|
|
883
|
+
if ('bigint' === typeof val) return `${val}`;
|
|
884
|
+
if (val !== null && 'object' === typeof val) {
|
|
885
|
+
const original = val;
|
|
886
|
+
if (stack.some(item => item.original === original)) {
|
|
887
|
+
circularCount++;
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
if (val instanceof Map || val instanceof Set) val = [...val];
|
|
891
|
+
stack.push({original, converted: val});
|
|
892
|
+
}
|
|
893
|
+
return val;
|
|
894
|
+
}, 2);
|
|
895
|
+
return json === undefined
|
|
896
|
+
? {error: 'This value can\'t be represented as JSON.', circularCount}
|
|
897
|
+
: {json, circularCount};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Stores a value as a global variable (temp1, temp2, etc.) like the
|
|
902
|
+
* browser's console does so that code can use it.
|
|
903
|
+
* @param {(string|number)[]} path
|
|
904
|
+
*/
|
|
905
|
+
function storeAsGlobal(path) {
|
|
906
|
+
const level = getLevel(path);
|
|
907
|
+
if (!level) return;
|
|
908
|
+
let index = 1;
|
|
909
|
+
while (`temp${index}` in globalThis) index++;
|
|
910
|
+
globalThis[`temp${index}`] = level.value;
|
|
911
|
+
send({target: 'viewer', func: 'onStoredAsGlobal', args: [`temp${index}`]});
|
|
817
912
|
}
|
|
818
913
|
|
|
819
914
|
/**
|
|
@@ -824,7 +919,7 @@
|
|
|
824
919
|
groupIds = [];
|
|
825
920
|
}
|
|
826
921
|
|
|
827
|
-
return {clearLogs, destroy, runCode, sendDescriptionFor};
|
|
922
|
+
return {clearLogs, destroy, getValueAsJson, refreshDescription, runCode, sendDescriptionFor, storeAsGlobal};
|
|
828
923
|
}
|
|
829
924
|
|
|
830
925
|
|
|
@@ -1199,6 +1294,18 @@
|
|
|
1199
1294
|
dividerSize: '8px',
|
|
1200
1295
|
tempDividerPct: null,
|
|
1201
1296
|
isMenuOpen: false,
|
|
1297
|
+
isHistoryOpen: false,
|
|
1298
|
+
// The code that was run (newest last) which can be brought back into
|
|
1299
|
+
// the editor like the browser's console (eg. with the up arrow).
|
|
1300
|
+
commandHistory: [],
|
|
1301
|
+
// While going through the history: the index of the code shown and
|
|
1302
|
+
// the unrun code that was in the editor (shown again at the end).
|
|
1303
|
+
historyIndex: null,
|
|
1304
|
+
historyDraft: null,
|
|
1305
|
+
/** @type {{x: number, y: number, path: any[], propertyPath: string?, isPrimitive: boolean}?} */
|
|
1306
|
+
valueMenu: null,
|
|
1307
|
+
/** @type {{message: string}?} */
|
|
1308
|
+
toast: null,
|
|
1202
1309
|
textScale: loadTextScale(),
|
|
1203
1310
|
isFullscreen: false,
|
|
1204
1311
|
// Set when full screen isn't allowed so the console fills the page.
|
|
@@ -1321,11 +1428,23 @@
|
|
|
1321
1428
|
bottomButtons() {
|
|
1322
1429
|
const isFullscreen = this.isFullscreen || this.isMaximized;
|
|
1323
1430
|
return [
|
|
1431
|
+
{
|
|
1432
|
+
iconName: 'more',
|
|
1433
|
+
title: 'More',
|
|
1434
|
+
className: 'more-button',
|
|
1435
|
+
callback() { this.toggleMenu(); },
|
|
1436
|
+
},
|
|
1324
1437
|
{
|
|
1325
1438
|
iconName: 'clear',
|
|
1326
1439
|
title: 'Clear console',
|
|
1327
1440
|
callback() { this.clearConsole(); },
|
|
1328
1441
|
},
|
|
1442
|
+
{
|
|
1443
|
+
iconName: 'history',
|
|
1444
|
+
title: `History (${isMac ? '\u2325' : 'Alt+'}H)`,
|
|
1445
|
+
className: 'history-button',
|
|
1446
|
+
callback() { this.toggleHistory(); },
|
|
1447
|
+
},
|
|
1329
1448
|
{
|
|
1330
1449
|
iconName: 'horizontalView',
|
|
1331
1450
|
title: 'Show the editor below the console',
|
|
@@ -1343,12 +1462,6 @@
|
|
|
1343
1462
|
title: isFullscreen ? 'Exit full screen' : 'Full screen',
|
|
1344
1463
|
callback() { this.toggleFullscreen(); },
|
|
1345
1464
|
},
|
|
1346
|
-
{
|
|
1347
|
-
iconName: 'more',
|
|
1348
|
-
title: 'More',
|
|
1349
|
-
className: 'more-button',
|
|
1350
|
-
callback() { this.toggleMenu(); },
|
|
1351
|
-
},
|
|
1352
1465
|
{
|
|
1353
1466
|
iconName: this.runningCount ? 'spinner' : 'play',
|
|
1354
1467
|
label: 'Run',
|
|
@@ -1359,6 +1472,20 @@
|
|
|
1359
1472
|
},
|
|
1360
1473
|
].filter(btn => !btn.showIf || btn.showIf.call(this));
|
|
1361
1474
|
},
|
|
1475
|
+
/**
|
|
1476
|
+
* The history's entries (newest first) for the history list.
|
|
1477
|
+
*/
|
|
1478
|
+
historyEntries() {
|
|
1479
|
+
return this.commandHistory.map((code, index) => {
|
|
1480
|
+
const [group] = parseJSCodeGroups(code);
|
|
1481
|
+
const lines = (group?.lines ?? code).split('\n').filter(line => line.trim());
|
|
1482
|
+
return {
|
|
1483
|
+
index,
|
|
1484
|
+
label: group?.headerLines || lines[0] || '',
|
|
1485
|
+
detail: `${lines.length} line${lines.length === 1 ? '' : 's'}`,
|
|
1486
|
+
};
|
|
1487
|
+
}).reverse();
|
|
1488
|
+
},
|
|
1362
1489
|
canShrinkText() {
|
|
1363
1490
|
return this.textScale > TEXT_SCALES[0];
|
|
1364
1491
|
},
|
|
@@ -1399,9 +1526,110 @@
|
|
|
1399
1526
|
this.runGroup(jsCodeGroup0);
|
|
1400
1527
|
|
|
1401
1528
|
// Remove the code that was run from the editor.
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
this.
|
|
1529
|
+
const remainingCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
|
|
1530
|
+
|
|
1531
|
+
if (this.historyIndex !== null) {
|
|
1532
|
+
// Code from the history was run so the unrun code comes back. It
|
|
1533
|
+
// doesn't count as moving through the code so hidden blocks still
|
|
1534
|
+
// run in their places.
|
|
1535
|
+
this.jsCode = [remainingCode, this.historyDraft].filter(code => code.trim()).join('\n');
|
|
1536
|
+
this.historyIndex = this.historyDraft = null;
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
this.jsCode = remainingCode;
|
|
1540
|
+
this.runCount++;
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
/**
|
|
1544
|
+
* Shows older (-1) or newer (1) code from the history in the editor.
|
|
1545
|
+
* Going newer than the newest shows the unrun code again.
|
|
1546
|
+
* @param {-1|1} direction
|
|
1547
|
+
* @returns {boolean}
|
|
1548
|
+
* `true` if the history was used (so the key shouldn't do anything
|
|
1549
|
+
* else).
|
|
1550
|
+
*/
|
|
1551
|
+
navigateHistory(direction) {
|
|
1552
|
+
const history = this.commandHistory;
|
|
1553
|
+
if (direction < 0) {
|
|
1554
|
+
if (!history.length) return false;
|
|
1555
|
+
if (this.historyIndex === null) {
|
|
1556
|
+
this.historyDraft = this.jsCode;
|
|
1557
|
+
this.historyIndex = history.length;
|
|
1558
|
+
}
|
|
1559
|
+
if (this.historyIndex > 0) this.showHistoryCode(this.historyIndex - 1, 'start');
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
if (this.historyIndex === null) return false;
|
|
1563
|
+
if (this.historyIndex < history.length - 1) {
|
|
1564
|
+
this.showHistoryCode(this.historyIndex + 1, 'end');
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
const draft = this.historyDraft;
|
|
1568
|
+
this.historyIndex = this.historyDraft = null;
|
|
1569
|
+
this.setEditorCode(draft, 'end');
|
|
1570
|
+
}
|
|
1571
|
+
return true;
|
|
1572
|
+
},
|
|
1573
|
+
/**
|
|
1574
|
+
* Shows code from the history in the editor (saving the unrun code if
|
|
1575
|
+
* this is the first time).
|
|
1576
|
+
* @param {number} index
|
|
1577
|
+
* @param {"start"|"end"} cursorAt
|
|
1578
|
+
*/
|
|
1579
|
+
showHistoryCode(index, cursorAt) {
|
|
1580
|
+
if (this.historyIndex === null) this.historyDraft = this.jsCode;
|
|
1581
|
+
this.historyIndex = index;
|
|
1582
|
+
this.setEditorCode(this.commandHistory[index], cursorAt);
|
|
1583
|
+
},
|
|
1584
|
+
/**
|
|
1585
|
+
* @param {string} code
|
|
1586
|
+
* @param {"start"|"end"} cursorAt
|
|
1587
|
+
* Where the cursor goes so that pressing the same arrow key again
|
|
1588
|
+
* keeps going through the history.
|
|
1589
|
+
*/
|
|
1590
|
+
setEditorCode(code, cursorAt) {
|
|
1591
|
+
this.jsCode = code;
|
|
1592
|
+
this.$nextTick(() => {
|
|
1593
|
+
const editor = this.$refs.editor?.editor;
|
|
1594
|
+
if (!editor) return;
|
|
1595
|
+
if (cursorAt === 'end') editor.navigateFileEnd();
|
|
1596
|
+
else editor.navigateFileStart();
|
|
1597
|
+
});
|
|
1598
|
+
},
|
|
1599
|
+
/**
|
|
1600
|
+
* Called by the editor when the up or down arrow is pressed with the
|
|
1601
|
+
* cursor at the very start or end (and nothing selected).
|
|
1602
|
+
* @param {{direction: -1|1, handled: boolean}} event
|
|
1603
|
+
*/
|
|
1604
|
+
onEditorHistoryKey(event) {
|
|
1605
|
+
event.handled = this.navigateHistory(event.direction);
|
|
1606
|
+
},
|
|
1607
|
+
toggleHistory() {
|
|
1608
|
+
if (this.isHistoryOpen) {
|
|
1609
|
+
this.isHistoryOpen = false;
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
this.closeMenu();
|
|
1613
|
+
this.closeValueMenu();
|
|
1614
|
+
this.isHistoryOpen = true;
|
|
1615
|
+
this.$nextTick(() => this.$refs.historyPanel?.querySelector('.menu-item')?.focus());
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
/**
|
|
1619
|
+
* Puts code from the history list into the editor.
|
|
1620
|
+
* @param {number} index
|
|
1621
|
+
*/
|
|
1622
|
+
recallHistory(index) {
|
|
1623
|
+
this.isHistoryOpen = false;
|
|
1624
|
+
this.showHistoryCode(index, 'start');
|
|
1625
|
+
this.$nextTick(() => this.$refs.editor?.editor.focus());
|
|
1626
|
+
},
|
|
1627
|
+
/**
|
|
1628
|
+
* Forgets that the history was being gone through (eg. when the code in
|
|
1629
|
+
* the editor is replaced).
|
|
1630
|
+
*/
|
|
1631
|
+
stopNavigatingHistory() {
|
|
1632
|
+
this.historyIndex = this.historyDraft = null;
|
|
1405
1633
|
},
|
|
1406
1634
|
/**
|
|
1407
1635
|
* Runs the next hidden group if all of the visible code that came
|
|
@@ -1429,6 +1657,12 @@
|
|
|
1429
1657
|
});
|
|
1430
1658
|
this.runningCount++;
|
|
1431
1659
|
this.runHistory.push(group.allLines);
|
|
1660
|
+
// Hidden code wasn't typed or seen so it isn't in the history.
|
|
1661
|
+
const historyCode = group.allLines.trim();
|
|
1662
|
+
if (!isHidden && historyCode && this.commandHistory[this.commandHistory.length - 1] !== historyCode) {
|
|
1663
|
+
this.commandHistory.push(historyCode);
|
|
1664
|
+
if (this.commandHistory.length > 200) this.commandHistory.shift();
|
|
1665
|
+
}
|
|
1432
1666
|
const {code, resultRange} = prepareCode(group.lines, {
|
|
1433
1667
|
blockType: this.blockType,
|
|
1434
1668
|
importsUrl: this.importsUrl,
|
|
@@ -1470,6 +1704,7 @@
|
|
|
1470
1704
|
this.runCount = 0;
|
|
1471
1705
|
this.runningCount = 0;
|
|
1472
1706
|
this.runHistory = [];
|
|
1707
|
+
this.stopNavigatingHistory();
|
|
1473
1708
|
messageParent({target: 'runner', func: 'reset', args: []});
|
|
1474
1709
|
this.runHiddenGroups();
|
|
1475
1710
|
},
|
|
@@ -1552,6 +1787,7 @@
|
|
|
1552
1787
|
});
|
|
1553
1788
|
if (!isConfirmed) return;
|
|
1554
1789
|
}
|
|
1790
|
+
this.stopNavigatingHistory();
|
|
1555
1791
|
this.jsCode = code;
|
|
1556
1792
|
},
|
|
1557
1793
|
/**
|
|
@@ -1637,6 +1873,9 @@
|
|
|
1637
1873
|
jsCode: this.jsCode,
|
|
1638
1874
|
originalCode: this.originalCode,
|
|
1639
1875
|
fileName: this.fileName,
|
|
1876
|
+
commandHistory: this.commandHistory,
|
|
1877
|
+
historyIndex: this.historyIndex,
|
|
1878
|
+
historyDraft: this.historyDraft,
|
|
1640
1879
|
runHistory: this.runHistory,
|
|
1641
1880
|
hiddenGroups: this.hiddenGroups,
|
|
1642
1881
|
runCount: this.runCount,
|
|
@@ -1658,6 +1897,8 @@
|
|
|
1658
1897
|
this.closeMenu();
|
|
1659
1898
|
}
|
|
1660
1899
|
else {
|
|
1900
|
+
this.isHistoryOpen = false;
|
|
1901
|
+
this.closeValueMenu();
|
|
1661
1902
|
this.isMenuOpen = true;
|
|
1662
1903
|
this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
|
|
1663
1904
|
}
|
|
@@ -1670,14 +1911,21 @@
|
|
|
1670
1911
|
* Lets the arrow keys move between the menu's items.
|
|
1671
1912
|
* @param {KeyboardEvent} evt
|
|
1672
1913
|
*/
|
|
1914
|
+
/**
|
|
1915
|
+
* Lets Escape close a menu (or the history list) and the arrow keys move
|
|
1916
|
+
* between its items.
|
|
1917
|
+
* @param {KeyboardEvent} evt
|
|
1918
|
+
*/
|
|
1673
1919
|
onMenuKeyDown(evt) {
|
|
1674
1920
|
if (evt.key === 'Escape') {
|
|
1675
1921
|
evt.stopPropagation();
|
|
1676
|
-
this.closeMenu(
|
|
1922
|
+
this.closeMenu(this.isMenuOpen);
|
|
1923
|
+
this.closeValueMenu();
|
|
1924
|
+
this.isHistoryOpen = false;
|
|
1677
1925
|
}
|
|
1678
1926
|
else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
|
|
1679
1927
|
evt.preventDefault();
|
|
1680
|
-
const items = [...
|
|
1928
|
+
const items = [...evt.currentTarget.querySelectorAll('.menu-item:not(:disabled)')];
|
|
1681
1929
|
const index = items.indexOf(document.activeElement);
|
|
1682
1930
|
const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
|
|
1683
1931
|
next?.focus();
|
|
@@ -1757,15 +2005,189 @@
|
|
|
1757
2005
|
if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
|
|
1758
2006
|
this.closeMenu();
|
|
1759
2007
|
}
|
|
2008
|
+
if (this.valueMenu && !evt.target.closest('.value-menu')) {
|
|
2009
|
+
this.closeValueMenu();
|
|
2010
|
+
}
|
|
2011
|
+
if (this.isHistoryOpen && !evt.target.closest('.history-panel, .history-button')) {
|
|
2012
|
+
this.isHistoryOpen = false;
|
|
2013
|
+
}
|
|
1760
2014
|
},
|
|
1761
2015
|
/**
|
|
1762
2016
|
* @param {KeyboardEvent} evt
|
|
1763
2017
|
*/
|
|
1764
2018
|
onWindowKeyDown(evt) {
|
|
2019
|
+
if (evt.altKey && !evt.ctrlKey && !evt.metaKey && evt.code === 'KeyH') {
|
|
2020
|
+
evt.preventDefault();
|
|
2021
|
+
evt.stopPropagation();
|
|
2022
|
+
this.toggleHistory();
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
if (evt.key === 'Escape' && this.valueMenu) {
|
|
2026
|
+
this.closeValueMenu();
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
1765
2029
|
if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
|
|
1766
2030
|
this.setMaximized(false);
|
|
1767
2031
|
}
|
|
1768
2032
|
},
|
|
2033
|
+
/**
|
|
2034
|
+
* Shows the menu for a value that was right-clicked.
|
|
2035
|
+
* @param {MouseEvent} evt
|
|
2036
|
+
* @param {{path: any[], keyPath: string[]?, isPrimitive: boolean}} value
|
|
2037
|
+
*/
|
|
2038
|
+
openValueMenu(evt, {path, keyPath, isPrimitive}) {
|
|
2039
|
+
this.closeMenu();
|
|
2040
|
+
this.valueMenu = {
|
|
2041
|
+
x: evt.clientX,
|
|
2042
|
+
y: evt.clientY,
|
|
2043
|
+
path: path.slice(),
|
|
2044
|
+
// Only properties and items (not logged values themselves) have
|
|
2045
|
+
// a property path.
|
|
2046
|
+
propertyPath: keyPath?.length ? formatPropertyPath(keyPath) : null,
|
|
2047
|
+
fileName: keyPath?.length && /^[\w$-]+$/.test(keyPath[keyPath.length - 1]) ? keyPath[keyPath.length - 1] : 'value',
|
|
2048
|
+
isPrimitive,
|
|
2049
|
+
};
|
|
2050
|
+
// Keeps the menu inside of the window.
|
|
2051
|
+
this.$nextTick(() => {
|
|
2052
|
+
const menu = this.$refs.valueMenu;
|
|
2053
|
+
if (!menu || !this.valueMenu) return;
|
|
2054
|
+
const {width, height} = menu.getBoundingClientRect();
|
|
2055
|
+
this.valueMenu.x = Math.max(4, Math.min(this.valueMenu.x, innerWidth - width - 4));
|
|
2056
|
+
this.valueMenu.y = Math.max(4, Math.min(this.valueMenu.y, innerHeight - height - 4));
|
|
2057
|
+
menu.querySelector('.menu-item')?.focus();
|
|
2058
|
+
});
|
|
2059
|
+
},
|
|
2060
|
+
closeValueMenu() {
|
|
2061
|
+
this.valueMenu = null;
|
|
2062
|
+
},
|
|
2063
|
+
/**
|
|
2064
|
+
* Asks the runner to convert the value to JSON.
|
|
2065
|
+
* @param {any[]} path
|
|
2066
|
+
* @returns {Promise<{json: string?, circularCount: number, error: string?}>}
|
|
2067
|
+
*/
|
|
2068
|
+
requestValueJson(path) {
|
|
2069
|
+
const requestId = `${Date.now()}-${Math.random()}`;
|
|
2070
|
+
return new Promise(resolve => {
|
|
2071
|
+
jsonRequests.set(requestId, resolve);
|
|
2072
|
+
messageParent({target: 'runner', func: 'getValueAsJson', args: [path, requestId]});
|
|
2073
|
+
});
|
|
2074
|
+
},
|
|
2075
|
+
/**
|
|
2076
|
+
* Shows a short message (eg. "Copied") for a few seconds.
|
|
2077
|
+
* @param {string} message
|
|
2078
|
+
*/
|
|
2079
|
+
showToast(message) {
|
|
2080
|
+
clearTimeout(this.toastTimeout);
|
|
2081
|
+
this.toast = {message};
|
|
2082
|
+
this.toastTimeout = setTimeout(() => this.toast = null, 3500);
|
|
2083
|
+
},
|
|
2084
|
+
/**
|
|
2085
|
+
* @param {{json: string?, circularCount: number, error: string?}} result
|
|
2086
|
+
* @param {string} action
|
|
2087
|
+
* What was done (eg. "Copied as JSON").
|
|
2088
|
+
*/
|
|
2089
|
+
showJsonToast({circularCount, error}, action) {
|
|
2090
|
+
this.showToast(
|
|
2091
|
+
error || action + (circularCount
|
|
2092
|
+
? ` (${circularCount} circular reference${circularCount === 1 ? ' was' : 's were'} left out)`
|
|
2093
|
+
: '')
|
|
2094
|
+
);
|
|
2095
|
+
},
|
|
2096
|
+
/**
|
|
2097
|
+
* Copies the value as JSON. The copy is started right away (while
|
|
2098
|
+
* the browser still allows it) with the JSON filled in when it is
|
|
2099
|
+
* ready.
|
|
2100
|
+
*/
|
|
2101
|
+
async copyValueAsJson() {
|
|
2102
|
+
// A plain copy because the menu's (reactive) path can't be sent.
|
|
2103
|
+
const path = [...this.valueMenu.path];
|
|
2104
|
+
this.closeValueMenu();
|
|
2105
|
+
const resultPromise = this.requestValueJson(path);
|
|
2106
|
+
try {
|
|
2107
|
+
if (window.ClipboardItem && navigator.clipboard?.write) {
|
|
2108
|
+
await navigator.clipboard.write([new ClipboardItem({
|
|
2109
|
+
'text/plain': resultPromise.then(({json, error}) => {
|
|
2110
|
+
if (error) throw new Error(error);
|
|
2111
|
+
return new Blob([json], {type: 'text/plain'});
|
|
2112
|
+
}),
|
|
2113
|
+
})]);
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
const {json, error} = await resultPromise;
|
|
2117
|
+
if (error) throw new Error(error);
|
|
2118
|
+
await navigator.clipboard.writeText(json);
|
|
2119
|
+
}
|
|
2120
|
+
this.showJsonToast(await resultPromise, 'Copied as JSON');
|
|
2121
|
+
}
|
|
2122
|
+
catch (e) {
|
|
2123
|
+
const result = await resultPromise;
|
|
2124
|
+
this.showToast(result.error || 'The value couldn\'t be copied. Your browser may not allow copying here.');
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
/**
|
|
2128
|
+
* Saves the value as a JSON file. The "Save as" dialog is shown right
|
|
2129
|
+
* away (while the browser still allows it).
|
|
2130
|
+
*/
|
|
2131
|
+
async saveValueAsJson() {
|
|
2132
|
+
const {fileName} = this.valueMenu;
|
|
2133
|
+
const path = [...this.valueMenu.path];
|
|
2134
|
+
this.closeValueMenu();
|
|
2135
|
+
const resultPromise = this.requestValueJson(path);
|
|
2136
|
+
const suggestedName = `${fileName}.json`;
|
|
2137
|
+
let handle;
|
|
2138
|
+
if ('function' === typeof window.showSaveFilePicker) {
|
|
2139
|
+
try {
|
|
2140
|
+
handle = await showSaveFilePicker({
|
|
2141
|
+
suggestedName,
|
|
2142
|
+
types: [{description: 'JSON', accept: {'application/json': ['.json']}}],
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
catch (e) {
|
|
2146
|
+
if (e?.name === 'AbortError') return;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
const result = await resultPromise;
|
|
2150
|
+
if (result.error) {
|
|
2151
|
+
this.showToast(result.error);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const text = result.json + '\n';
|
|
2155
|
+
if (handle) {
|
|
2156
|
+
const writable = await handle.createWritable();
|
|
2157
|
+
await writable.write(text);
|
|
2158
|
+
await writable.close();
|
|
2159
|
+
}
|
|
2160
|
+
else {
|
|
2161
|
+
const url = URL.createObjectURL(new Blob([text], {type: 'application/json'}));
|
|
2162
|
+
Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
|
|
2163
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
2164
|
+
}
|
|
2165
|
+
this.showJsonToast(result, 'Saved as JSON');
|
|
2166
|
+
},
|
|
2167
|
+
storeValueAsGlobal() {
|
|
2168
|
+
const path = [...this.valueMenu.path];
|
|
2169
|
+
this.closeValueMenu();
|
|
2170
|
+
messageParent({target: 'runner', func: 'storeAsGlobal', args: [path]});
|
|
2171
|
+
},
|
|
2172
|
+
async copyPropertyPath() {
|
|
2173
|
+
const {propertyPath} = this.valueMenu;
|
|
2174
|
+
this.closeValueMenu();
|
|
2175
|
+
try {
|
|
2176
|
+
await navigator.clipboard.writeText(propertyPath);
|
|
2177
|
+
this.showToast(`Copied ${propertyPath}`);
|
|
2178
|
+
}
|
|
2179
|
+
catch (e) {
|
|
2180
|
+
this.showToast('The property path couldn\'t be copied. Your browser may not allow copying here.');
|
|
2181
|
+
}
|
|
2182
|
+
},
|
|
2183
|
+
/**
|
|
2184
|
+
* Shows the value as it is now (eg. after the code changed it).
|
|
2185
|
+
*/
|
|
2186
|
+
refreshValue() {
|
|
2187
|
+
const path = [...this.valueMenu.path];
|
|
2188
|
+
this.closeValueMenu();
|
|
2189
|
+
messageParent({target: 'runner', func: 'refreshDescription', args: [path]});
|
|
2190
|
+
},
|
|
1769
2191
|
closeDialog(isConfirmed) {
|
|
1770
2192
|
const {dialog} = this;
|
|
1771
2193
|
this.dialog = null;
|
|
@@ -1786,6 +2208,7 @@
|
|
|
1786
2208
|
}
|
|
1787
2209
|
},
|
|
1788
2210
|
onDisplaysScroll() {
|
|
2211
|
+
this.closeValueMenu();
|
|
1789
2212
|
const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
|
|
1790
2213
|
this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
|
|
1791
2214
|
},
|
|
@@ -1859,7 +2282,8 @@
|
|
|
1859
2282
|
mounted() {
|
|
1860
2283
|
addEventListener('mousemove', this.onWindowMouseMove);
|
|
1861
2284
|
addEventListener('mousedown', this.onWindowMouseDown);
|
|
1862
|
-
|
|
2285
|
+
// Capturing lets shortcuts (eg. Alt+H) work before the editor sees them.
|
|
2286
|
+
addEventListener('keydown', this.onWindowKeyDown, true);
|
|
1863
2287
|
addEventListener('resize', this.onWindowResize);
|
|
1864
2288
|
addEventListener('mouseup', this.onWindowMouseUp);
|
|
1865
2289
|
addEventListener('error', this.onWindowError);
|
|
@@ -2005,6 +2429,28 @@
|
|
|
2005
2429
|
});
|
|
2006
2430
|
|
|
2007
2431
|
// Attempt to capture key combos.
|
|
2432
|
+
// Like the browser's console, the up arrow at the very start (or the
|
|
2433
|
+
// down arrow at the very end) with nothing selected goes through the
|
|
2434
|
+
// history. This listens while capturing so it runs before Ace moves the
|
|
2435
|
+
// cursor.
|
|
2436
|
+
this.$refs.editor.addEventListener('keydown', evt => {
|
|
2437
|
+
const isUp = evt.key === 'ArrowUp';
|
|
2438
|
+
if (!isUp && evt.key !== 'ArrowDown') return;
|
|
2439
|
+
if (evt.shiftKey || evt.altKey || evt.ctrlKey || evt.metaKey || !editor.selection.isEmpty()) return;
|
|
2440
|
+
const {row, column} = editor.getCursorPosition();
|
|
2441
|
+
const lastRow = editor.session.getLength() - 1;
|
|
2442
|
+
const isAtEdge = isUp
|
|
2443
|
+
? row === 0 && column === 0
|
|
2444
|
+
: row === lastRow && column === editor.session.getLine(lastRow).length;
|
|
2445
|
+
if (!isAtEdge) return;
|
|
2446
|
+
const event = {direction: isUp ? -1 : 1, handled: false};
|
|
2447
|
+
this.$emit('historyKey', event);
|
|
2448
|
+
if (event.handled) {
|
|
2449
|
+
evt.preventDefault();
|
|
2450
|
+
evt.stopPropagation();
|
|
2451
|
+
}
|
|
2452
|
+
}, true);
|
|
2453
|
+
|
|
2008
2454
|
editor.textInput.getElement().addEventListener('keydown', (evt) => {
|
|
2009
2455
|
const isComboKeyCode = evt.keyCode === 16 // SHIFT
|
|
2010
2456
|
|| evt.keyCode === 17 // CTRL
|
|
@@ -2109,6 +2555,10 @@
|
|
|
2109
2555
|
// https://icon-sets.iconify.design/mdi/error-outline/
|
|
2110
2556
|
error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
|
|
2111
2557
|
// Icons similar to those in the browser's console.
|
|
2558
|
+
history: '<svg viewBox="0 0 16 16"><path d="M2.5 8a5.5 5.5 0 1 0 1.6-3.9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M2.25 2.5v2.75H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 5v3.25l2.25 1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
2559
|
+
copy: '<svg viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 3.5v-.75A1.25 1.25 0 0 0 9.25 1.5h-5A1.25 1.25 0 0 0 3 2.75v5A1.25 1.25 0 0 0 4.25 9h.75" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
|
|
2560
|
+
variable: '<svg viewBox="0 0 16 16"><path d="M5 2.5c-1.5 1.5-2 3.5-2 5.5s.5 4 2 5.5M11 2.5c1.5 1.5 2 3.5 2 5.5s-.5 4-2 5.5M6.5 6l3 4M9.5 6l-3 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
|
|
2561
|
+
path: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="12.5" r="1.5" fill="currentColor"/><circle cx="12.5" cy="3.5" r="1.5" fill="currentColor"/><path d="M3.5 11V8a2 2 0 0 1 2-2h5a2 2 0 0 0 2-2" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
|
|
2112
2562
|
open: '<svg viewBox="0 0 16 16"><path d="M2 4.5a1 1 0 0 1 1-1h3.2l1.3 1.5H13a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>',
|
|
2113
2563
|
save: '<svg viewBox="0 0 16 16"><path d="M8 2.5v7M5 7l3 3 3-3M3 11.5v1.5h10v-1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
2114
2564
|
fullscreen: '<svg viewBox="0 0 16 16"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
@@ -2136,7 +2586,10 @@
|
|
|
2136
2586
|
|
|
2137
2587
|
function getJSValueComponentProps() {
|
|
2138
2588
|
return {
|
|
2139
|
-
|
|
2589
|
+
// keyPath is the property keys leading to this value from the logged
|
|
2590
|
+
// value (eg. ["people", "0", "name"]) or null if it can't be reached by
|
|
2591
|
+
// property keys (eg. inside of a Map).
|
|
2592
|
+
props: ['description', 'path', 'name', 'isDimName', 'keyPath'],
|
|
2140
2593
|
data() {
|
|
2141
2594
|
return {
|
|
2142
2595
|
isExpanded: false,
|
|
@@ -2148,7 +2601,12 @@
|
|
|
2148
2601
|
if (newValue && !this.hasBeenExpanded) {
|
|
2149
2602
|
this.hasBeenExpanded = newValue;
|
|
2150
2603
|
}
|
|
2151
|
-
}
|
|
2604
|
+
},
|
|
2605
|
+
// When a refresh replaces this value's description, its new contents are
|
|
2606
|
+
// requested if it is still expanded.
|
|
2607
|
+
isPartialDescription(isPartial) {
|
|
2608
|
+
if (isPartial && this.isExpanded) this.requestDescription();
|
|
2609
|
+
},
|
|
2152
2610
|
},
|
|
2153
2611
|
computed: {
|
|
2154
2612
|
isPartialDescription() {
|
|
@@ -2169,10 +2627,20 @@
|
|
|
2169
2627
|
];
|
|
2170
2628
|
},
|
|
2171
2629
|
entryGroups() {
|
|
2172
|
-
// protoEntries only ever contains the [[Prototype]] entry
|
|
2630
|
+
// protoEntries only ever contains the [[Prototype]] entry whose
|
|
2631
|
+
// members can be reached directly so it adds nothing to the key path.
|
|
2632
|
+
const {keyPath, description} = this;
|
|
2173
2633
|
return [
|
|
2174
|
-
{
|
|
2175
|
-
|
|
2634
|
+
{
|
|
2635
|
+
key: 'entries',
|
|
2636
|
+
isDim: entry => entry[2] === false,
|
|
2637
|
+
getKeyPath: entry => keyPath && description.entriesArePropertyKeys ? keyPath.concat([entry[0]]) : null,
|
|
2638
|
+
},
|
|
2639
|
+
{
|
|
2640
|
+
key: 'protoEntries',
|
|
2641
|
+
isDim: () => true,
|
|
2642
|
+
getKeyPath: () => keyPath,
|
|
2643
|
+
},
|
|
2176
2644
|
];
|
|
2177
2645
|
}
|
|
2178
2646
|
},
|
|
@@ -2180,14 +2648,26 @@
|
|
|
2180
2648
|
toggleExpanded() {
|
|
2181
2649
|
if (!this.isExpandable) return;
|
|
2182
2650
|
this.isExpanded = !this.isExpanded;
|
|
2183
|
-
if (this.isExpanded && this.isPartialDescription)
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2651
|
+
if (this.isExpanded && this.isPartialDescription) this.requestDescription();
|
|
2652
|
+
},
|
|
2653
|
+
requestDescription() {
|
|
2654
|
+
messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
|
|
2655
|
+
},
|
|
2656
|
+
/**
|
|
2657
|
+
* Shows the menu for this value (eg. Copy as JSON).
|
|
2658
|
+
* @param {MouseEvent} evt
|
|
2659
|
+
*/
|
|
2660
|
+
openMenu(evt) {
|
|
2661
|
+
this.$root.openValueMenu(evt, {
|
|
2662
|
+
path: this.path,
|
|
2663
|
+
keyPath: this.keyPath,
|
|
2664
|
+
isPrimitive: this.description.isPrimitive,
|
|
2665
|
+
});
|
|
2666
|
+
},
|
|
2187
2667
|
},
|
|
2188
2668
|
template: `
|
|
2189
2669
|
<div :class="classNames">
|
|
2190
|
-
<div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
|
|
2670
|
+
<div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded" @contextmenu.prevent.stop="openMenu"><span
|
|
2191
2671
|
v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
|
|
2192
2672
|
v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
|
|
2193
2673
|
v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
|
|
@@ -2200,6 +2680,7 @@
|
|
|
2200
2680
|
:name="entry[0]"
|
|
2201
2681
|
:is-dim-name="group.isDim(entry)"
|
|
2202
2682
|
:description="entry[1]"
|
|
2683
|
+
:key-path="group.getKeyPath(entry)"
|
|
2203
2684
|
:path="path.concat([group.key, entryIndex])">
|
|
2204
2685
|
</js-value>
|
|
2205
2686
|
</template>
|
|
@@ -2367,6 +2848,22 @@
|
|
|
2367
2848
|
return {code, resultRange};
|
|
2368
2849
|
}
|
|
2369
2850
|
|
|
2851
|
+
/**
|
|
2852
|
+
* Turns property keys into a path like the browser's console's "Copy property
|
|
2853
|
+
* path" (eg. ["people", "0", "first name"] becomes `people[0]["first name"]`).
|
|
2854
|
+
* @param {string[]} keys
|
|
2855
|
+
* @returns {string}
|
|
2856
|
+
*/
|
|
2857
|
+
function formatPropertyPath(keys) {
|
|
2858
|
+
return keys.map((key, index) =>
|
|
2859
|
+
/^(0|[1-9]\d*)$/.test(key)
|
|
2860
|
+
? `[${key}]`
|
|
2861
|
+
: /^[A-Za-z_$][\w$]*$/.test(key)
|
|
2862
|
+
? (index ? '.' : '') + key
|
|
2863
|
+
: `[${JSON.stringify(key)}]`
|
|
2864
|
+
).join('');
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2370
2867
|
/**
|
|
2371
2868
|
* Indicates if an import specifier is a package name (eg. "lodash",
|
|
2372
2869
|
* "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
|
|
@@ -2525,6 +3022,7 @@
|
|
|
2525
3022
|
`${Object(part)[1]}`,
|
|
2526
3023
|
]),
|
|
2527
3024
|
...sanitizeEntries(description),
|
|
3025
|
+
...(description.entriesArePropertyKeys != null ? {entriesArePropertyKeys: !!description.entriesArePropertyKeys} : {}),
|
|
2528
3026
|
};
|
|
2529
3027
|
}
|
|
2530
3028
|
|
|
@@ -2615,7 +3113,47 @@
|
|
|
2615
3113
|
level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
|
|
2616
3114
|
}
|
|
2617
3115
|
|
|
2618
|
-
if (level)
|
|
3116
|
+
if (!level) return;
|
|
3117
|
+
Object.assign(level, sanitizeEntries(description));
|
|
3118
|
+
level.entriesArePropertyKeys = !!Object(description).entriesArePropertyKeys;
|
|
3119
|
+
// A refresh also sends the value's preview.
|
|
3120
|
+
if (Array.isArray(Object(description).parts)) {
|
|
3121
|
+
const {typeName, isPrimitive, parts} = sanitizeDescription(description);
|
|
3122
|
+
Object.assign(level, {typeName, isPrimitive, parts});
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
/**
|
|
3127
|
+
* NOTE: Called via main by the runner with a value converted to JSON (see
|
|
3128
|
+
* requestValueJson()).
|
|
3129
|
+
* @param {{requestId: string, json?: string, circularCount?: number, error?: string}} result
|
|
3130
|
+
*/
|
|
3131
|
+
function onValueJson(result) {
|
|
3132
|
+
const {requestId, json, circularCount, error} = Object(result);
|
|
3133
|
+
const resolve = jsonRequests.get(`${requestId}`);
|
|
3134
|
+
if (!resolve) return;
|
|
3135
|
+
jsonRequests.delete(`${requestId}`);
|
|
3136
|
+
resolve({
|
|
3137
|
+
json: json != null ? `${json}` : null,
|
|
3138
|
+
circularCount: +circularCount || 0,
|
|
3139
|
+
error: error != null ? `${error}` : null,
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
/**
|
|
3144
|
+
* The functions waiting for values to be converted to JSON by their request
|
|
3145
|
+
* IDs.
|
|
3146
|
+
* @type {Map<string, (result: {json: string?, circularCount: number, error: string?}) => void>}
|
|
3147
|
+
*/
|
|
3148
|
+
const jsonRequests = new Map();
|
|
3149
|
+
|
|
3150
|
+
/**
|
|
3151
|
+
* NOTE: Called via main by the runner after a value was stored as a global
|
|
3152
|
+
* variable.
|
|
3153
|
+
* @param {string} name
|
|
3154
|
+
*/
|
|
3155
|
+
function onStoredAsGlobal(name) {
|
|
3156
|
+
mountedApp.displays.push({type: 'notice', message: `Stored as the global variable ${`${name}`}`});
|
|
2619
3157
|
}
|
|
2620
3158
|
|
|
2621
3159
|
/**
|