yourjs-box 1.4.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 +75 -7
- package/dist/yourjs-box.full.js +806 -80
- package/dist/yourjs-box.js +39 -12
- package/dist/yourjs-box.min.js +6 -5
- package/package.json +4 -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}.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><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 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>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.
|
|
@@ -62,14 +62,35 @@
|
|
|
62
62
|
return new URL(baseUrl + path, document.baseURI).href;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Where packages imported by name (eg. `import _ from 'lodash'`) are loaded
|
|
67
|
+
* from unless data-imports-url is given. `{specifier}` is replaced with what
|
|
68
|
+
* was imported (eg. "lodash@4/fp").
|
|
69
|
+
*/
|
|
70
|
+
const DEFAULT_IMPORTS_URL = 'https://esm.sh/{specifier}';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Makes the part of a URL template before its first placeholder absolute
|
|
74
|
+
* (relative to the page) because the code runs somewhere (eg. a worker)
|
|
75
|
+
* that doesn't know the page's URL.
|
|
76
|
+
* @param {string} template
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function resolveUrlTemplate(template) {
|
|
80
|
+
const index = template.indexOf('{');
|
|
81
|
+
const prefix = index < 0 ? template : template.slice(0, index);
|
|
82
|
+
return new URL(prefix || './', document.baseURI).href.replace(/\/$/, prefix.endsWith('/') || !prefix ? '/' : '')
|
|
83
|
+
+ (index < 0 ? '' : template.slice(index));
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
/**
|
|
66
87
|
* The only functions that may be relayed to the viewer and to the runner.
|
|
67
88
|
* The runner executes the user's code so anything it sends must be limited
|
|
68
89
|
* to these calls.
|
|
69
90
|
*/
|
|
70
91
|
const RELAYABLE_FUNCS = {
|
|
71
|
-
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
|
|
72
|
-
runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
|
|
92
|
+
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'onStoredAsGlobal', 'onValueJson', 'updateDescriptionFor'],
|
|
93
|
+
runner: ['clearLogs', 'getValueAsJson', 'refreshDescription', 'reset', 'runCode', 'sendDescriptionFor', 'storeAsGlobal'],
|
|
73
94
|
// Things that the viewer (but never the runner) can ask this script to do.
|
|
74
95
|
host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
|
|
75
96
|
};
|
|
@@ -285,10 +306,12 @@
|
|
|
285
306
|
* @param {boolean=} isInPromise
|
|
286
307
|
*/
|
|
287
308
|
function reportUncaught(error, isInPromise) {
|
|
288
|
-
// Points out how to use top-level await
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
: ''
|
|
309
|
+
// Points out how to use top-level await or import statements if they were
|
|
310
|
+
// used in a classic block.
|
|
311
|
+
const feature = runningBlockType === 'classic' && error?.name === 'SyntaxError'
|
|
312
|
+
? /\bawait\b/.test(error.message) ? 'top-level await' : /\bimport\b/.test(error.message) ? 'import statements' : null
|
|
313
|
+
: null;
|
|
314
|
+
const hint = feature ? `\n(To use ${feature}, add data-block-type="module" to the script tag.)` : '';
|
|
292
315
|
send({
|
|
293
316
|
target: 'viewer',
|
|
294
317
|
func: 'appendError',
|
|
@@ -608,9 +631,9 @@
|
|
|
608
631
|
const $entries = [];
|
|
609
632
|
const protoEntries = [];
|
|
610
633
|
const $protoEntries = [];
|
|
634
|
+
let wasIterated = false;
|
|
611
635
|
|
|
612
636
|
if (value !== null && ('object' === typeof value || 'function' === typeof value)) {
|
|
613
|
-
let wasIterated = false;
|
|
614
637
|
try {
|
|
615
638
|
// Map
|
|
616
639
|
if (typeName === 'Map') {
|
|
@@ -658,7 +681,9 @@
|
|
|
658
681
|
}
|
|
659
682
|
}
|
|
660
683
|
|
|
661
|
-
|
|
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};
|
|
662
687
|
}
|
|
663
688
|
|
|
664
689
|
// ['123', 0, 'entries', 0]
|
|
@@ -769,16 +794,25 @@
|
|
|
769
794
|
* entry indices.
|
|
770
795
|
*/
|
|
771
796
|
function sendDescriptionFor(path) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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);
|
|
782
816
|
if (!level) return;
|
|
783
817
|
const description = describe(level.value, 'receiver' in level ? level.receiver : level.value);
|
|
784
818
|
|
|
@@ -786,11 +820,95 @@
|
|
|
786
820
|
Object.assign(level, description);
|
|
787
821
|
|
|
788
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
|
+
: {};
|
|
789
826
|
send({
|
|
790
827
|
target: 'viewer',
|
|
791
828
|
func: 'updateDescriptionFor',
|
|
792
|
-
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];
|
|
793
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}`]});
|
|
794
912
|
}
|
|
795
913
|
|
|
796
914
|
/**
|
|
@@ -801,7 +919,7 @@
|
|
|
801
919
|
groupIds = [];
|
|
802
920
|
}
|
|
803
921
|
|
|
804
|
-
return {clearLogs, destroy, runCode, sendDescriptionFor};
|
|
922
|
+
return {clearLogs, destroy, getValueAsJson, refreshDescription, runCode, sendDescriptionFor, storeAsGlobal};
|
|
805
923
|
}
|
|
806
924
|
|
|
807
925
|
|
|
@@ -928,6 +1046,10 @@
|
|
|
928
1046
|
const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
|
|
929
1047
|
const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
|
|
930
1048
|
const showResults = dataset.showResults !== 'false';
|
|
1049
|
+
// An empty data-imports-url turns off loading packages by name.
|
|
1050
|
+
const importsUrl = dataset.importsUrl == null
|
|
1051
|
+
? DEFAULT_IMPORTS_URL
|
|
1052
|
+
: dataset.importsUrl && resolveUrlTemplate(dataset.importsUrl);
|
|
931
1053
|
const libraryUrl = getLibraryFileUrl.bind(null, dataset.librariesUrl || DEFAULT_LIBRARIES_URL);
|
|
932
1054
|
|
|
933
1055
|
// The viewer in the page and, when the console is popped out, the viewer
|
|
@@ -1117,8 +1239,8 @@
|
|
|
1117
1239
|
|
|
1118
1240
|
const hidePrefix = dataset.hidePrefix ?? '';
|
|
1119
1241
|
const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
|
|
1120
|
-
const
|
|
1121
|
-
const {visibleCode, hiddenGroups} = extractHiddenGroups(
|
|
1242
|
+
const startingCode = unindentMin(jsCode);
|
|
1243
|
+
const {visibleCode, hiddenGroups} = extractHiddenGroups(startingCode, hidePrefix);
|
|
1122
1244
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
|
|
1123
1245
|
const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
|
|
1124
1246
|
|
|
@@ -1130,6 +1252,12 @@
|
|
|
1130
1252
|
hidePrefix,
|
|
1131
1253
|
hiddenGroups: copyHiddenGroups(),
|
|
1132
1254
|
runnerMode: meta.runnerMode,
|
|
1255
|
+
importsUrl: meta.importsUrl,
|
|
1256
|
+
// The code the console starts with (and goes back to when reset)
|
|
1257
|
+
// which changes when a file is opened.
|
|
1258
|
+
originalCode: startingCode,
|
|
1259
|
+
// The name of the file that was last opened or saved.
|
|
1260
|
+
fileName: null,
|
|
1133
1261
|
blockType: meta.blockType,
|
|
1134
1262
|
showResults: meta.showResults,
|
|
1135
1263
|
packageInfo: meta.packageInfo,
|
|
@@ -1166,6 +1294,18 @@
|
|
|
1166
1294
|
dividerSize: '8px',
|
|
1167
1295
|
tempDividerPct: null,
|
|
1168
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,
|
|
1169
1309
|
textScale: loadTextScale(),
|
|
1170
1310
|
isFullscreen: false,
|
|
1171
1311
|
// Set when full screen isn't allowed so the console fills the page.
|
|
@@ -1233,12 +1373,23 @@
|
|
|
1233
1373
|
/**
|
|
1234
1374
|
* The code that the copied console will start with.
|
|
1235
1375
|
*/
|
|
1376
|
+
/**
|
|
1377
|
+
* The original code split into the code for the editor and the
|
|
1378
|
+
* hidden groups.
|
|
1379
|
+
*/
|
|
1380
|
+
originalGroups() {
|
|
1381
|
+
return extractHiddenGroups(this.originalCode, this.hidePrefix);
|
|
1382
|
+
},
|
|
1236
1383
|
exportJsCode() {
|
|
1237
|
-
if (this.exportCode === 'original') return originalCode;
|
|
1384
|
+
if (this.exportCode === 'original') return this.originalCode;
|
|
1238
1385
|
if (this.exportCode === 'blank') return '';
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1386
|
+
return this.currentCode;
|
|
1387
|
+
},
|
|
1388
|
+
/**
|
|
1389
|
+
* The groups that already ran, then the groups in the editor with any
|
|
1390
|
+
* hidden groups that haven't run yet in their original places.
|
|
1391
|
+
*/
|
|
1392
|
+
currentCode() {
|
|
1242
1393
|
const blocks = this.runHistory.slice();
|
|
1243
1394
|
const pendingHiddenGroups = this.hiddenGroups.slice();
|
|
1244
1395
|
let visibleCount = this.runCount;
|
|
@@ -1277,11 +1428,23 @@
|
|
|
1277
1428
|
bottomButtons() {
|
|
1278
1429
|
const isFullscreen = this.isFullscreen || this.isMaximized;
|
|
1279
1430
|
return [
|
|
1431
|
+
{
|
|
1432
|
+
iconName: 'more',
|
|
1433
|
+
title: 'More',
|
|
1434
|
+
className: 'more-button',
|
|
1435
|
+
callback() { this.toggleMenu(); },
|
|
1436
|
+
},
|
|
1280
1437
|
{
|
|
1281
1438
|
iconName: 'clear',
|
|
1282
1439
|
title: 'Clear console',
|
|
1283
1440
|
callback() { this.clearConsole(); },
|
|
1284
1441
|
},
|
|
1442
|
+
{
|
|
1443
|
+
iconName: 'history',
|
|
1444
|
+
title: `History (${isMac ? '\u2325' : 'Alt+'}H)`,
|
|
1445
|
+
className: 'history-button',
|
|
1446
|
+
callback() { this.toggleHistory(); },
|
|
1447
|
+
},
|
|
1285
1448
|
{
|
|
1286
1449
|
iconName: 'horizontalView',
|
|
1287
1450
|
title: 'Show the editor below the console',
|
|
@@ -1299,12 +1462,6 @@
|
|
|
1299
1462
|
title: isFullscreen ? 'Exit full screen' : 'Full screen',
|
|
1300
1463
|
callback() { this.toggleFullscreen(); },
|
|
1301
1464
|
},
|
|
1302
|
-
{
|
|
1303
|
-
iconName: 'more',
|
|
1304
|
-
title: 'More',
|
|
1305
|
-
className: 'more-button',
|
|
1306
|
-
callback() { this.toggleMenu(); },
|
|
1307
|
-
},
|
|
1308
1465
|
{
|
|
1309
1466
|
iconName: this.runningCount ? 'spinner' : 'play',
|
|
1310
1467
|
label: 'Run',
|
|
@@ -1315,6 +1472,20 @@
|
|
|
1315
1472
|
},
|
|
1316
1473
|
].filter(btn => !btn.showIf || btn.showIf.call(this));
|
|
1317
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
|
+
},
|
|
1318
1489
|
canShrinkText() {
|
|
1319
1490
|
return this.textScale > TEXT_SCALES[0];
|
|
1320
1491
|
},
|
|
@@ -1355,9 +1526,110 @@
|
|
|
1355
1526
|
this.runGroup(jsCodeGroup0);
|
|
1356
1527
|
|
|
1357
1528
|
// Remove the code that was run from the editor.
|
|
1358
|
-
|
|
1529
|
+
const remainingCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
|
|
1359
1530
|
|
|
1360
|
-
this.
|
|
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;
|
|
1361
1633
|
},
|
|
1362
1634
|
/**
|
|
1363
1635
|
* Runs the next hidden group if all of the visible code that came
|
|
@@ -1385,17 +1657,23 @@
|
|
|
1385
1657
|
});
|
|
1386
1658
|
this.runningCount++;
|
|
1387
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
|
+
}
|
|
1666
|
+
const {code, resultRange} = prepareCode(group.lines, {
|
|
1667
|
+
blockType: this.blockType,
|
|
1668
|
+
importsUrl: this.importsUrl,
|
|
1669
|
+
// Like the browser's console, show the value of the last
|
|
1670
|
+
// expression (except for hidden code).
|
|
1671
|
+
findResult: this.showResults && !isHidden,
|
|
1672
|
+
});
|
|
1388
1673
|
messageParent({
|
|
1389
1674
|
target: 'runner',
|
|
1390
1675
|
func: 'runCode',
|
|
1391
|
-
args: [
|
|
1392
|
-
blockType: this.blockType,
|
|
1393
|
-
// Like the browser's console, show the value of the last
|
|
1394
|
-
// expression (except for hidden code).
|
|
1395
|
-
resultRange: this.showResults && !isHidden
|
|
1396
|
-
? findLastExpression(group.lines, this.blockType)
|
|
1397
|
-
: null,
|
|
1398
|
-
}],
|
|
1676
|
+
args: [code, {blockType: this.blockType, resultRange}],
|
|
1399
1677
|
});
|
|
1400
1678
|
},
|
|
1401
1679
|
clearConsole() {
|
|
@@ -1412,16 +1690,88 @@
|
|
|
1412
1690
|
confirmText: 'Reset',
|
|
1413
1691
|
});
|
|
1414
1692
|
if (!isConfirmed) return;
|
|
1415
|
-
|
|
1693
|
+
this.restart();
|
|
1694
|
+
},
|
|
1695
|
+
/**
|
|
1696
|
+
* Starts over with the original code (clearing the output and, in
|
|
1697
|
+
* worker mode, starting a new worker).
|
|
1698
|
+
*/
|
|
1699
|
+
restart() {
|
|
1700
|
+
const {visibleCode, hiddenGroups} = this.originalGroups;
|
|
1416
1701
|
this.displays = [];
|
|
1417
1702
|
this.jsCode = visibleCode;
|
|
1418
|
-
this.hiddenGroups =
|
|
1703
|
+
this.hiddenGroups = hiddenGroups.map(group => ({...group}));
|
|
1419
1704
|
this.runCount = 0;
|
|
1420
1705
|
this.runningCount = 0;
|
|
1421
1706
|
this.runHistory = [];
|
|
1707
|
+
this.stopNavigatingHistory();
|
|
1422
1708
|
messageParent({target: 'runner', func: 'reset', args: []});
|
|
1423
1709
|
this.runHiddenGroups();
|
|
1424
1710
|
},
|
|
1711
|
+
/**
|
|
1712
|
+
* Shows the file picker for opening a JavaScript file.
|
|
1713
|
+
*/
|
|
1714
|
+
openFile() {
|
|
1715
|
+
this.closeMenu();
|
|
1716
|
+
const {fileInput} = this.$refs;
|
|
1717
|
+
fileInput.value = '';
|
|
1718
|
+
fileInput.click();
|
|
1719
|
+
},
|
|
1720
|
+
/**
|
|
1721
|
+
* Makes the chosen file's code the code that the console starts with.
|
|
1722
|
+
* @param {Event} evt
|
|
1723
|
+
*/
|
|
1724
|
+
async onFileChosen(evt) {
|
|
1725
|
+
const file = evt.target.files?.[0];
|
|
1726
|
+
if (!file) return;
|
|
1727
|
+
const code = unindentMin(await file.text());
|
|
1728
|
+
if (this.displays.length || this.jsCode.trim()) {
|
|
1729
|
+
const isConfirmed = await this.confirm({
|
|
1730
|
+
title: `Open ${file.name}?`,
|
|
1731
|
+
message: 'The output will be cleared and the code in the editor will be replaced with the code in the file.'
|
|
1732
|
+
+ (this.runnerMode === 'window'
|
|
1733
|
+
? ' Anything the code already defined on the page will stay defined.'
|
|
1734
|
+
: ''),
|
|
1735
|
+
confirmText: 'Open',
|
|
1736
|
+
});
|
|
1737
|
+
if (!isConfirmed) return;
|
|
1738
|
+
}
|
|
1739
|
+
this.fileName = file.name;
|
|
1740
|
+
this.originalCode = code;
|
|
1741
|
+
this.restart();
|
|
1742
|
+
},
|
|
1743
|
+
/**
|
|
1744
|
+
* Saves the current code (what already ran and what is in the editor)
|
|
1745
|
+
* as a JavaScript file which can be opened later.
|
|
1746
|
+
*/
|
|
1747
|
+
async saveFile() {
|
|
1748
|
+
this.closeMenu();
|
|
1749
|
+
const code = this.currentCode + '\n';
|
|
1750
|
+
const suggestedName = this.fileName || 'js-box.js';
|
|
1751
|
+
|
|
1752
|
+
// Shows a "Save as" dialog where it is supported.
|
|
1753
|
+
if ('function' === typeof window.showSaveFilePicker) {
|
|
1754
|
+
try {
|
|
1755
|
+
const handle = await showSaveFilePicker({
|
|
1756
|
+
suggestedName,
|
|
1757
|
+
types: [{description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}}],
|
|
1758
|
+
});
|
|
1759
|
+
const writable = await handle.createWritable();
|
|
1760
|
+
await writable.write(code);
|
|
1761
|
+
await writable.close();
|
|
1762
|
+
this.fileName = handle.name;
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
catch (e) {
|
|
1766
|
+
// The user cancelled. For other errors the file is downloaded.
|
|
1767
|
+
if (e?.name === 'AbortError') return;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const url = URL.createObjectURL(new Blob([code], {type: 'text/javascript'}));
|
|
1772
|
+
Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
|
|
1773
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1774
|
+
},
|
|
1425
1775
|
/**
|
|
1426
1776
|
* Copies code that was already run into the editor so that it can be run
|
|
1427
1777
|
* again (as is or modified), first asking before replacing any code that
|
|
@@ -1437,6 +1787,7 @@
|
|
|
1437
1787
|
});
|
|
1438
1788
|
if (!isConfirmed) return;
|
|
1439
1789
|
}
|
|
1790
|
+
this.stopNavigatingHistory();
|
|
1440
1791
|
this.jsCode = code;
|
|
1441
1792
|
},
|
|
1442
1793
|
/**
|
|
@@ -1520,6 +1871,11 @@
|
|
|
1520
1871
|
return JSON.parse(JSON.stringify({
|
|
1521
1872
|
displays: this.displays,
|
|
1522
1873
|
jsCode: this.jsCode,
|
|
1874
|
+
originalCode: this.originalCode,
|
|
1875
|
+
fileName: this.fileName,
|
|
1876
|
+
commandHistory: this.commandHistory,
|
|
1877
|
+
historyIndex: this.historyIndex,
|
|
1878
|
+
historyDraft: this.historyDraft,
|
|
1523
1879
|
runHistory: this.runHistory,
|
|
1524
1880
|
hiddenGroups: this.hiddenGroups,
|
|
1525
1881
|
runCount: this.runCount,
|
|
@@ -1541,6 +1897,8 @@
|
|
|
1541
1897
|
this.closeMenu();
|
|
1542
1898
|
}
|
|
1543
1899
|
else {
|
|
1900
|
+
this.isHistoryOpen = false;
|
|
1901
|
+
this.closeValueMenu();
|
|
1544
1902
|
this.isMenuOpen = true;
|
|
1545
1903
|
this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
|
|
1546
1904
|
}
|
|
@@ -1553,14 +1911,21 @@
|
|
|
1553
1911
|
* Lets the arrow keys move between the menu's items.
|
|
1554
1912
|
* @param {KeyboardEvent} evt
|
|
1555
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
|
+
*/
|
|
1556
1919
|
onMenuKeyDown(evt) {
|
|
1557
1920
|
if (evt.key === 'Escape') {
|
|
1558
1921
|
evt.stopPropagation();
|
|
1559
|
-
this.closeMenu(
|
|
1922
|
+
this.closeMenu(this.isMenuOpen);
|
|
1923
|
+
this.closeValueMenu();
|
|
1924
|
+
this.isHistoryOpen = false;
|
|
1560
1925
|
}
|
|
1561
1926
|
else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
|
|
1562
1927
|
evt.preventDefault();
|
|
1563
|
-
const items = [...
|
|
1928
|
+
const items = [...evt.currentTarget.querySelectorAll('.menu-item:not(:disabled)')];
|
|
1564
1929
|
const index = items.indexOf(document.activeElement);
|
|
1565
1930
|
const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
|
|
1566
1931
|
next?.focus();
|
|
@@ -1640,15 +2005,189 @@
|
|
|
1640
2005
|
if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
|
|
1641
2006
|
this.closeMenu();
|
|
1642
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
|
+
}
|
|
1643
2014
|
},
|
|
1644
2015
|
/**
|
|
1645
2016
|
* @param {KeyboardEvent} evt
|
|
1646
2017
|
*/
|
|
1647
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
|
+
}
|
|
1648
2029
|
if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
|
|
1649
2030
|
this.setMaximized(false);
|
|
1650
2031
|
}
|
|
1651
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
|
+
},
|
|
1652
2191
|
closeDialog(isConfirmed) {
|
|
1653
2192
|
const {dialog} = this;
|
|
1654
2193
|
this.dialog = null;
|
|
@@ -1669,6 +2208,7 @@
|
|
|
1669
2208
|
}
|
|
1670
2209
|
},
|
|
1671
2210
|
onDisplaysScroll() {
|
|
2211
|
+
this.closeValueMenu();
|
|
1672
2212
|
const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
|
|
1673
2213
|
this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
|
|
1674
2214
|
},
|
|
@@ -1742,7 +2282,8 @@
|
|
|
1742
2282
|
mounted() {
|
|
1743
2283
|
addEventListener('mousemove', this.onWindowMouseMove);
|
|
1744
2284
|
addEventListener('mousedown', this.onWindowMouseDown);
|
|
1745
|
-
|
|
2285
|
+
// Capturing lets shortcuts (eg. Alt+H) work before the editor sees them.
|
|
2286
|
+
addEventListener('keydown', this.onWindowKeyDown, true);
|
|
1746
2287
|
addEventListener('resize', this.onWindowResize);
|
|
1747
2288
|
addEventListener('mouseup', this.onWindowMouseUp);
|
|
1748
2289
|
addEventListener('error', this.onWindowError);
|
|
@@ -1888,6 +2429,28 @@
|
|
|
1888
2429
|
});
|
|
1889
2430
|
|
|
1890
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
|
+
|
|
1891
2454
|
editor.textInput.getElement().addEventListener('keydown', (evt) => {
|
|
1892
2455
|
const isComboKeyCode = evt.keyCode === 16 // SHIFT
|
|
1893
2456
|
|| evt.keyCode === 17 // CTRL
|
|
@@ -1992,6 +2555,12 @@
|
|
|
1992
2555
|
// https://icon-sets.iconify.design/mdi/error-outline/
|
|
1993
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>',
|
|
1994
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>',
|
|
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>',
|
|
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>',
|
|
1995
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>',
|
|
1996
2565
|
exitFullscreen: '<svg viewBox="0 0 16 16"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1997
2566
|
more: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="1.4" fill="currentColor"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/><circle cx="12.5" cy="8" r="1.4" fill="currentColor"/></svg>',
|
|
@@ -2017,7 +2586,10 @@
|
|
|
2017
2586
|
|
|
2018
2587
|
function getJSValueComponentProps() {
|
|
2019
2588
|
return {
|
|
2020
|
-
|
|
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'],
|
|
2021
2593
|
data() {
|
|
2022
2594
|
return {
|
|
2023
2595
|
isExpanded: false,
|
|
@@ -2029,7 +2601,12 @@
|
|
|
2029
2601
|
if (newValue && !this.hasBeenExpanded) {
|
|
2030
2602
|
this.hasBeenExpanded = newValue;
|
|
2031
2603
|
}
|
|
2032
|
-
}
|
|
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
|
+
},
|
|
2033
2610
|
},
|
|
2034
2611
|
computed: {
|
|
2035
2612
|
isPartialDescription() {
|
|
@@ -2050,10 +2627,20 @@
|
|
|
2050
2627
|
];
|
|
2051
2628
|
},
|
|
2052
2629
|
entryGroups() {
|
|
2053
|
-
// 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;
|
|
2054
2633
|
return [
|
|
2055
|
-
{
|
|
2056
|
-
|
|
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
|
+
},
|
|
2057
2644
|
];
|
|
2058
2645
|
}
|
|
2059
2646
|
},
|
|
@@ -2061,14 +2648,26 @@
|
|
|
2061
2648
|
toggleExpanded() {
|
|
2062
2649
|
if (!this.isExpandable) return;
|
|
2063
2650
|
this.isExpanded = !this.isExpanded;
|
|
2064
|
-
if (this.isExpanded && this.isPartialDescription)
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
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
|
+
},
|
|
2068
2667
|
},
|
|
2069
2668
|
template: `
|
|
2070
2669
|
<div :class="classNames">
|
|
2071
|
-
<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
|
|
2072
2671
|
v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
|
|
2073
2672
|
v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
|
|
2074
2673
|
v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
|
|
@@ -2081,6 +2680,7 @@
|
|
|
2081
2680
|
:name="entry[0]"
|
|
2082
2681
|
:is-dim-name="group.isDim(entry)"
|
|
2083
2682
|
:description="entry[1]"
|
|
2683
|
+
:key-path="group.getKeyPath(entry)"
|
|
2084
2684
|
:path="path.concat([group.key, entryIndex])">
|
|
2085
2685
|
</js-value>
|
|
2086
2686
|
</template>
|
|
@@ -2184,30 +2784,113 @@
|
|
|
2184
2784
|
}
|
|
2185
2785
|
|
|
2186
2786
|
/**
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2787
|
+
* Gets a block of code ready to run: packages imported by name (eg.
|
|
2788
|
+
* `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
|
|
2789
|
+
* and the last statement is found if it is an expression so that its value
|
|
2790
|
+
* can be shown.
|
|
2189
2791
|
* @param {string} code
|
|
2190
|
-
* @param {
|
|
2191
|
-
* @
|
|
2192
|
-
*
|
|
2193
|
-
*
|
|
2194
|
-
*
|
|
2792
|
+
* @param {Object} options
|
|
2793
|
+
* @param {"classic"|"module"} options.blockType
|
|
2794
|
+
* @param {string?} options.importsUrl
|
|
2795
|
+
* The URL template where `{specifier}` is replaced with what was imported
|
|
2796
|
+
* or an empty value to leave imports alone.
|
|
2797
|
+
* @param {boolean} options.findResult
|
|
2798
|
+
* @returns {{code: string, resultRange: [number, number]|null}}
|
|
2799
|
+
* The code to run and the start and end index of its last expression (or
|
|
2800
|
+
* `null`). If the code can't be parsed it is returned as is so that running
|
|
2801
|
+
* it shows the syntax error.
|
|
2195
2802
|
*/
|
|
2196
|
-
function
|
|
2197
|
-
if (!window.acorn) return null;
|
|
2803
|
+
function prepareCode(code, {blockType, importsUrl, findResult}) {
|
|
2804
|
+
if (!window.acorn) return {code, resultRange: null};
|
|
2805
|
+
let ast;
|
|
2198
2806
|
try {
|
|
2199
|
-
|
|
2807
|
+
ast = acorn.parse(code, {
|
|
2200
2808
|
ecmaVersion: 'latest',
|
|
2201
2809
|
sourceType: blockType === 'module' ? 'module' : 'script',
|
|
2202
2810
|
allowHashBang: true,
|
|
2203
2811
|
});
|
|
2204
|
-
|
|
2812
|
+
}
|
|
2813
|
+
catch (e) {
|
|
2814
|
+
return {code, resultRange: null};
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
let resultRange = null;
|
|
2818
|
+
if (findResult) {
|
|
2819
|
+
const last = ast.body[ast.body.length - 1];
|
|
2205
2820
|
if (last?.type === 'ExpressionStatement' && !last.directive) {
|
|
2206
|
-
|
|
2821
|
+
resultRange = [last.expression.start, last.expression.end];
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// Finds every import (and re-export) of a package by name.
|
|
2826
|
+
const replacements = [];
|
|
2827
|
+
if (importsUrl) {
|
|
2828
|
+
walkAst(ast, node => {
|
|
2829
|
+
const {source} = /^(Import(Declaration|Expression)|Export(All|Named)Declaration)$/.test(node.type) ? node : {};
|
|
2830
|
+
if (source?.type === 'Literal' && 'string' === typeof source.value && isBareSpecifier(source.value)) {
|
|
2831
|
+
replacements.push({
|
|
2832
|
+
start: source.start,
|
|
2833
|
+
end: source.end,
|
|
2834
|
+
text: JSON.stringify(importsUrl.replace(/\{specifier\}/g, source.value)),
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// Replaces them from last to first, moving the result's range as needed.
|
|
2841
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
2842
|
+
for (const {start, end, text} of replacements) {
|
|
2843
|
+
code = code.slice(0, start) + text + code.slice(end);
|
|
2844
|
+
const shift = text.length - (end - start);
|
|
2845
|
+
if (resultRange) resultRange = resultRange.map(index => index >= end ? index + shift : index);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
return {code, resultRange};
|
|
2849
|
+
}
|
|
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
|
+
|
|
2867
|
+
/**
|
|
2868
|
+
* Indicates if an import specifier is a package name (eg. "lodash",
|
|
2869
|
+
* "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
|
|
2870
|
+
* "./utils.js", "https://example.com/x.js" or "node:fs").
|
|
2871
|
+
* @param {string} specifier
|
|
2872
|
+
* @returns {boolean}
|
|
2873
|
+
*/
|
|
2874
|
+
function isBareSpecifier(specifier) {
|
|
2875
|
+
return !/^(\.{0,2}\/|#|[a-z][a-z\d+.-]*:)/i.test(specifier);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/**
|
|
2879
|
+
* Calls the callback for every node in an Acorn syntax tree.
|
|
2880
|
+
* @param {*} node
|
|
2881
|
+
* @param {(node: *) => void} callback
|
|
2882
|
+
*/
|
|
2883
|
+
function walkAst(node, callback) {
|
|
2884
|
+
if (Array.isArray(node)) {
|
|
2885
|
+
for (const child of node) walkAst(child, callback);
|
|
2886
|
+
}
|
|
2887
|
+
else if (node && 'string' === typeof node.type) {
|
|
2888
|
+
callback(node);
|
|
2889
|
+
for (const key of Object.keys(node)) {
|
|
2890
|
+
const value = node[key];
|
|
2891
|
+
if (value && 'object' === typeof value) walkAst(value, callback);
|
|
2207
2892
|
}
|
|
2208
2893
|
}
|
|
2209
|
-
catch (e) {}
|
|
2210
|
-
return null;
|
|
2211
2894
|
}
|
|
2212
2895
|
|
|
2213
2896
|
/**
|
|
@@ -2339,6 +3022,7 @@
|
|
|
2339
3022
|
`${Object(part)[1]}`,
|
|
2340
3023
|
]),
|
|
2341
3024
|
...sanitizeEntries(description),
|
|
3025
|
+
...(description.entriesArePropertyKeys != null ? {entriesArePropertyKeys: !!description.entriesArePropertyKeys} : {}),
|
|
2342
3026
|
};
|
|
2343
3027
|
}
|
|
2344
3028
|
|
|
@@ -2429,7 +3113,47 @@
|
|
|
2429
3113
|
level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
|
|
2430
3114
|
}
|
|
2431
3115
|
|
|
2432
|
-
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}`}`});
|
|
2433
3157
|
}
|
|
2434
3158
|
|
|
2435
3159
|
/**
|
|
@@ -2496,9 +3220,9 @@
|
|
|
2496
3220
|
libraryUrl('prismjs', 'components/prism-core.min.js'),
|
|
2497
3221
|
libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
|
|
2498
3222
|
libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
|
|
2499
|
-
// Used to find the last expression in each block of code so that its
|
|
2500
|
-
// value can be shown.
|
|
2501
|
-
|
|
3223
|
+
// Used to find the last expression in each block of code (so that its
|
|
3224
|
+
// value can be shown) and the packages that it imports.
|
|
3225
|
+
libraryUrl('acorn', 'dist/acorn.js'),
|
|
2502
3226
|
],
|
|
2503
3227
|
cssUrls: [
|
|
2504
3228
|
'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
|
|
@@ -2518,6 +3242,7 @@
|
|
|
2518
3242
|
runnerMode,
|
|
2519
3243
|
blockType,
|
|
2520
3244
|
showResults,
|
|
3245
|
+
importsUrl,
|
|
2521
3246
|
packageInfo: PACKAGE_INFO,
|
|
2522
3247
|
libraryVersions: LIBRARY_VERSIONS,
|
|
2523
3248
|
isPopOut: !!targetWindow,
|
|
@@ -2592,7 +3317,7 @@
|
|
|
2592
3317
|
* The options of YourJSBox.create() which are the same as the data
|
|
2593
3318
|
* attributes of a script tag.
|
|
2594
3319
|
*/
|
|
2595
|
-
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
3320
|
+
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
2596
3321
|
|
|
2597
3322
|
/**
|
|
2598
3323
|
* The JavaScript API for creating consoles (available as window.YourJSBox).
|
|
@@ -2623,6 +3348,7 @@
|
|
|
2623
3348
|
* @param {string=} options.dividerOrient
|
|
2624
3349
|
* @param {string=} options.theme
|
|
2625
3350
|
* @param {string=} options.librariesUrl
|
|
3351
|
+
* @param {string=} options.importsUrl
|
|
2626
3352
|
* The same as the data attributes of a script tag.
|
|
2627
3353
|
* @returns {{element: HTMLIFrameElement, destroy: () => void}}
|
|
2628
3354
|
*/
|