yourjs-box 1.1.2 → 1.3.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/README.md +83 -11
- package/dist/yourjs-box.full.js +1812 -1139
- package/dist/yourjs-box.js +233 -51
- package/dist/yourjs-box.min.js +5 -4
- package/package.json +3 -1
package/dist/yourjs-box.full.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/*! yourjs-box v1.
|
|
1
|
+
/*! yourjs-box v1.3.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
|
|
2
2
|
(() => {
|
|
3
3
|
/**
|
|
4
4
|
* Viewer IFRAME's CSS code
|
|
5
5
|
* @type {string}
|
|
6
6
|
*/
|
|
7
|
-
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:12px;line-height:16px}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:12px!important;line-height:16px!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
7
|
+
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
8
8
|
/**
|
|
9
9
|
* Information about this package (eg. its version).
|
|
10
10
|
* @type {{name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}
|
|
11
11
|
*/
|
|
12
|
-
const PACKAGE_INFO = {"name":"yourjs-box","version":"1.
|
|
12
|
+
const PACKAGE_INFO = {"name":"yourjs-box","version":"1.3.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></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'\" :class=\"['console-row', 'log-' + display.key]\" :title=\"display.name\"><span v-if=\"display.key === 'error'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></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></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf && bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window</dd></dl><div class=\"about-footer\">MIT License © 2023-present Christopher West · Built with Vue, Ace and Prism</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><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' && !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf && bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A−</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitemradio\" :aria-checked=\"dividerOrient === 'vertical'\" @click=\"setDividerOrient('vertical'); closeMenu(true)\"><span class=\"menu-icon\"><icon v-if=\"dividerOrient === 'vertical'\" name=\"check\"></icon></span>Editor beside the console</button> <button class=\"menu-item\" role=\"menuitemradio\" :aria-checked=\"dividerOrient === 'horizontal'\" @click=\"setDividerOrient('horizontal'); closeMenu(true)\"><span class=\"menu-icon\"><icon v-if=\"dividerOrient === 'horizontal'\" name=\"check\"></icon></span>Editor below the console</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>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>";
|
|
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.
|
|
@@ -28,6 +28,40 @@
|
|
|
28
28
|
*/
|
|
29
29
|
const ORIGINAL_CONSOLE = window[Symbol.for('yourjs-box.originalConsole')] ??= {...console};
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The libraries that the viewer loads along with the exact versions that it
|
|
33
|
+
* was tested with.
|
|
34
|
+
*/
|
|
35
|
+
const LIBRARY_VERSIONS = {
|
|
36
|
+
'ace-builds': '1.44.0',
|
|
37
|
+
'acorn': '8.18.0',
|
|
38
|
+
'prism-themes': '1.9.0',
|
|
39
|
+
'prismjs': '1.30.0',
|
|
40
|
+
'vue': '3.5.43',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where the libraries are loaded from unless data-libraries-url is given.
|
|
45
|
+
* `{name}` and `{version}` are replaced with each library's name and version.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_LIBRARIES_URL = 'https://unpkg.com/{name}@{version}/';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} librariesUrl
|
|
51
|
+
* The URL template (see DEFAULT_LIBRARIES_URL). Relative URLs are relative
|
|
52
|
+
* to the page.
|
|
53
|
+
* @param {keyof LIBRARY_VERSIONS} name
|
|
54
|
+
* @param {string} path
|
|
55
|
+
* The path of the file within the library's package.
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function getLibraryFileUrl(librariesUrl, name, path) {
|
|
59
|
+
const baseUrl = librariesUrl
|
|
60
|
+
.replace(/\{(name|version)\}/g, (_, key) => key === 'name' ? name : LIBRARY_VERSIONS[name])
|
|
61
|
+
.replace(/\/?$/, '/');
|
|
62
|
+
return new URL(baseUrl + path, document.baseURI).href;
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
/**
|
|
32
66
|
* The only functions that may be relayed to the viewer and to the runner.
|
|
33
67
|
* The runner executes the user's code so anything it sends must be limited
|
|
@@ -36,6 +70,8 @@
|
|
|
36
70
|
const RELAYABLE_FUNCS = {
|
|
37
71
|
viewer: ['appendLog', 'appendError', 'clearDisplays', 'onCodeRan', 'updateDescriptionFor'],
|
|
38
72
|
runner: ['clearLogs', 'reset', 'runCode', 'sendDescriptionFor'],
|
|
73
|
+
// Things that the viewer (but never the runner) can ask this script to do.
|
|
74
|
+
host: ['focusPopOut', 'popIn', 'popOut', 'requestPopIn', 'setMaximized'],
|
|
39
75
|
};
|
|
40
76
|
|
|
41
77
|
/**
|
|
@@ -76,42 +112,143 @@
|
|
|
76
112
|
let logCount = 0;
|
|
77
113
|
let snippetCount = 0;
|
|
78
114
|
let canImportDataUrls = true;
|
|
115
|
+
// The type of the block of code that is running (see runCode()).
|
|
116
|
+
let runningBlockType;
|
|
117
|
+
// How wrapping the last expression of each block shifted the columns on its
|
|
118
|
+
// line so that stack traces can show the original columns.
|
|
119
|
+
const columnShiftsBySnippet = {};
|
|
120
|
+
|
|
121
|
+
const counts = {};
|
|
122
|
+
const timers = {};
|
|
123
|
+
// The IDs of the groups (from console.group()) that are currently open.
|
|
124
|
+
let groupIds = [];
|
|
125
|
+
let groupCount = 0;
|
|
126
|
+
|
|
127
|
+
// Called by the code that is run (see runCode()) with the value of the last
|
|
128
|
+
// expression. The key is unique to this runner because in window mode more
|
|
129
|
+
// than one console can be on the same page.
|
|
130
|
+
const RESULT_KEY = `yourjs-box.result.${Math.random().toString(36).slice(2)}`;
|
|
131
|
+
globalThis[Symbol.for(RESULT_KEY)] = reportResult;
|
|
79
132
|
|
|
80
133
|
// Overrides for console functions. In window mode this also captures
|
|
81
134
|
// anything else that the page logs, just like the browser's console.
|
|
82
|
-
for (const key of [
|
|
135
|
+
for (const key of [
|
|
136
|
+
'assert', 'clear', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error',
|
|
137
|
+
'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'table', 'time',
|
|
138
|
+
'timeEnd', 'timeLog', 'trace', 'warn',
|
|
139
|
+
]) {
|
|
83
140
|
const original = console[key];
|
|
84
141
|
if ('function' !== typeof original) continue;
|
|
85
142
|
console[key] = function(...args) {
|
|
86
|
-
|
|
87
|
-
clearLogs();
|
|
88
|
-
send({target: 'viewer', func: 'clearDisplays', args: [true]});
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
const table = key === 'table' ? parseTable(args[0], args[1]) : null;
|
|
143
|
+
handleConsoleCall(key, args);
|
|
92
144
|
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
// Calls and returns the original console function.
|
|
146
|
+
return original.apply(this, arguments);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
95
149
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Shows what was passed to a console function in the same way that the
|
|
152
|
+
* browser's console would.
|
|
153
|
+
* @param {string} key
|
|
154
|
+
* @param {any[]} args
|
|
155
|
+
*/
|
|
156
|
+
function handleConsoleCall(key, args) {
|
|
157
|
+
const label = args.length && args[0] !== undefined ? `${args[0]}` : 'default';
|
|
99
158
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
159
|
+
if (key === 'clear') {
|
|
160
|
+
clearLogs();
|
|
161
|
+
groupIds = [];
|
|
162
|
+
send({target: 'viewer', func: 'clearDisplays', args: [true]});
|
|
163
|
+
}
|
|
164
|
+
else if (key === 'assert') {
|
|
165
|
+
if (args[0]) return;
|
|
166
|
+
const rest = args.slice(1);
|
|
167
|
+
addLog('assert', !rest.length
|
|
168
|
+
? ['Assertion failed: console.assert']
|
|
169
|
+
: 'string' === typeof rest[0]
|
|
170
|
+
? ['Assertion failed: ' + rest[0], ...rest.slice(1)]
|
|
171
|
+
: ['Assertion failed:', ...rest]);
|
|
172
|
+
}
|
|
173
|
+
else if (key === 'count') {
|
|
174
|
+
counts[label] = (counts[label] || 0) + 1;
|
|
175
|
+
addLog('count', [`${label}: ${counts[label]}`]);
|
|
176
|
+
}
|
|
177
|
+
else if (key === 'countReset') {
|
|
178
|
+
if (Object.hasOwn(counts, label)) counts[label] = 0;
|
|
179
|
+
else addLog('warn', [`Count for '${label}' does not exist`]);
|
|
180
|
+
}
|
|
181
|
+
else if (key === 'time') {
|
|
182
|
+
if (Object.hasOwn(timers, label)) addLog('warn', [`Timer '${label}' already exists`]);
|
|
183
|
+
else timers[label] = performance.now();
|
|
184
|
+
}
|
|
185
|
+
else if (key === 'timeLog' || key === 'timeEnd') {
|
|
186
|
+
if (!Object.hasOwn(timers, label)) {
|
|
187
|
+
addLog('warn', [`Timer '${label}' does not exist`]);
|
|
188
|
+
return;
|
|
110
189
|
}
|
|
190
|
+
const ms = performance.now() - timers[label];
|
|
191
|
+
if (key === 'timeEnd') delete timers[label];
|
|
192
|
+
addLog(key, [`${label}: ${+ms.toFixed(3)} ms`, ...(key === 'timeLog' ? args.slice(1) : [])]);
|
|
193
|
+
}
|
|
194
|
+
else if (key === 'trace') {
|
|
195
|
+
// Removes the first line ("Error") and this runner's own lines.
|
|
196
|
+
const stack = cleanStack(new Error().stack ?? '').split('\n').slice(1).join('\n');
|
|
197
|
+
addLog('trace', args.length ? args : ['console.trace'], {stack});
|
|
198
|
+
}
|
|
199
|
+
else if (key === 'group' || key === 'groupCollapsed') {
|
|
200
|
+
const groupId = `${++groupCount}`;
|
|
201
|
+
addLog(key, args.length ? args : ['console.group'], {groupId, isCollapsed: key === 'groupCollapsed'});
|
|
202
|
+
groupIds.push(groupId);
|
|
203
|
+
}
|
|
204
|
+
else if (key === 'groupEnd') {
|
|
205
|
+
groupIds.pop();
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const table = key === 'table' ? parseTable(args[0], args[1]) : null;
|
|
111
209
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
}
|
|
210
|
+
// console.table() only shows the data that was tabulated.
|
|
211
|
+
addLog(key, table ? args.slice(0, 1) : args, {table});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Sends a message to show in the console.
|
|
217
|
+
* @param {string} key
|
|
218
|
+
* The name of the console function (eg. "log").
|
|
219
|
+
* @param {any[]} args
|
|
220
|
+
* @param {Object=} extra
|
|
221
|
+
* Other properties to send along with the message.
|
|
222
|
+
* @param {number=} depth
|
|
223
|
+
* Optional, defaults to `0`. The depth used to summarize the args (see
|
|
224
|
+
* summarize()).
|
|
225
|
+
*/
|
|
226
|
+
function addLog(key, args, extra, depth = 0) {
|
|
227
|
+
// Keep track of the args so that they can be expanded later.
|
|
228
|
+
const logId = `${++logCount}`;
|
|
229
|
+
logArgsById[logId] = args.map(value => ({...summarize(value, depth), value}));
|
|
230
|
+
|
|
231
|
+
send({
|
|
232
|
+
target: 'viewer',
|
|
233
|
+
func: 'appendLog',
|
|
234
|
+
args: [{
|
|
235
|
+
logId,
|
|
236
|
+
key,
|
|
237
|
+
descriptions: logArgsById[logId].map(without(['value'])),
|
|
238
|
+
groupIds: groupIds.slice(),
|
|
239
|
+
...extra,
|
|
240
|
+
}]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Shows the value of the last expression in the code that was run unless
|
|
246
|
+
* it is `undefined`.
|
|
247
|
+
* @param {*} value
|
|
248
|
+
*/
|
|
249
|
+
function reportResult(value) {
|
|
250
|
+
// Like the browser, strings are quoted (but logged strings aren't).
|
|
251
|
+
if (value !== undefined) addLog('result', [value], {}, 'string' === typeof value ? 1 : 0);
|
|
115
252
|
}
|
|
116
253
|
|
|
117
254
|
addEventListener('error', evt => {
|
|
@@ -127,12 +264,17 @@
|
|
|
127
264
|
* @param {boolean=} isInPromise
|
|
128
265
|
*/
|
|
129
266
|
function reportUncaught(error, isInPromise) {
|
|
267
|
+
// Points out how to use top-level await if it was used in a classic block.
|
|
268
|
+
const hint = runningBlockType === 'classic' && error?.name === 'SyntaxError' && /\bawait\b/.test(error.message)
|
|
269
|
+
? '\n(To use top-level await, add data-block-type="module" to the script tag.)'
|
|
270
|
+
: '';
|
|
130
271
|
send({
|
|
131
272
|
target: 'viewer',
|
|
132
273
|
func: 'appendError',
|
|
133
274
|
args: [{
|
|
134
275
|
message: (isInPromise ? 'Uncaught (in promise) ' : 'Uncaught ')
|
|
135
|
-
+ cleanStack(error?.stack ?? `${error?.message ?? error}`)
|
|
276
|
+
+ cleanStack(error?.stack ?? `${error?.message ?? error}`)
|
|
277
|
+
+ hint,
|
|
136
278
|
}]
|
|
137
279
|
});
|
|
138
280
|
}
|
|
@@ -147,7 +289,14 @@
|
|
|
147
289
|
return `${stack}`
|
|
148
290
|
.split('\n')
|
|
149
291
|
.filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
|
|
150
|
-
.join('\n')
|
|
292
|
+
.join('\n')
|
|
293
|
+
// Undoes the shift in columns caused by wrapping the last expression.
|
|
294
|
+
.replace(/(snippet-\d+\.js):(\d+):(\d+)/g, (match, name, line, column) => {
|
|
295
|
+
const info = columnShiftsBySnippet[name];
|
|
296
|
+
return info && +line === info.line && +column > info.column
|
|
297
|
+
? `${name}:${line}:${Math.max(info.column, column - info.shift)}`
|
|
298
|
+
: match;
|
|
299
|
+
});
|
|
151
300
|
}
|
|
152
301
|
|
|
153
302
|
function without(props, obj) {
|
|
@@ -278,7 +427,7 @@
|
|
|
278
427
|
function getPreviewParts(value, depth, typeName = getTypeName(value)) {
|
|
279
428
|
if (typeName === 'string') {
|
|
280
429
|
if (!depth) return [['text', value]];
|
|
281
|
-
if (depth > 1 && value.length > 100) value = value.slice(0, 99) + '
|
|
430
|
+
if (depth > 1 && value.length > 100) value = value.slice(0, 99) + '\u2026';
|
|
282
431
|
return [['string', quote(value)]];
|
|
283
432
|
}
|
|
284
433
|
if (typeName === 'number') return [['number', Object.is(value, -0) ? '-0' : '' + value]];
|
|
@@ -325,10 +474,10 @@
|
|
|
325
474
|
// Like the browser, show the source of functions passed directly to a
|
|
326
475
|
// console function.
|
|
327
476
|
if (!depth) {
|
|
328
|
-
return [['text', isClass ? source : source.replace(/^(async\s+)?function\b\s*/, '$1
|
|
477
|
+
return [['text', isClass ? source : source.replace(/^(async\s+)?function\b\s*/, '$1\u0192 ')]];
|
|
329
478
|
}
|
|
330
|
-
if (depth > 1) return [['function', '
|
|
331
|
-
return [['function', isClass ? `class ${value.name}` :
|
|
479
|
+
if (depth > 1) return [['function', '\u0192']];
|
|
480
|
+
return [['function', isClass ? `class ${value.name}` : `\u0192 ${value.name}()`]];
|
|
332
481
|
}
|
|
333
482
|
|
|
334
483
|
/**
|
|
@@ -345,6 +494,8 @@
|
|
|
345
494
|
return [['text', isNaN(value) ? 'Invalid Date' : Date.prototype.toString.call(value)]];
|
|
346
495
|
}
|
|
347
496
|
if (typeName === 'RegExp') return [['regexp', '' + value]];
|
|
497
|
+
// The state of a promise can't be determined synchronously.
|
|
498
|
+
if (typeName === 'Promise') return [['text', `${className} {\u2026}`]];
|
|
348
499
|
// DOM nodes (only available in window mode) are shown like the browser
|
|
349
500
|
// shows them in previews (eg. "h2#title.big").
|
|
350
501
|
if ('undefined' !== typeof Node && value instanceof Node) {
|
|
@@ -364,7 +515,7 @@
|
|
|
364
515
|
if (depth > 1) {
|
|
365
516
|
if (isArrayLike) return [['text', `${className}(${value.length})`]];
|
|
366
517
|
if (typeName === 'Map' || typeName === 'Set') return [['text', `${className}(${value.size})`]];
|
|
367
|
-
return [['text', className === 'Object' ? '{
|
|
518
|
+
return [['text', className === 'Object' ? '{\u2026}' : className]];
|
|
368
519
|
}
|
|
369
520
|
|
|
370
521
|
const parts = [];
|
|
@@ -373,7 +524,7 @@
|
|
|
373
524
|
if (index) parts.push(['text', ', ']);
|
|
374
525
|
addItem(item);
|
|
375
526
|
});
|
|
376
|
-
if (total > items.length) parts.push(['text', (items.length ? ', ' : '') + '
|
|
527
|
+
if (total > items.length) parts.push(['text', (items.length ? ', ' : '') + '\u2026']);
|
|
377
528
|
};
|
|
378
529
|
|
|
379
530
|
if (isArrayLike) {
|
|
@@ -413,7 +564,7 @@
|
|
|
413
564
|
parts.push(
|
|
414
565
|
['key', key],
|
|
415
566
|
['text', ': '],
|
|
416
|
-
...('value' in descriptor ? getPreviewParts(descriptor.value, 2) : [['text', '(
|
|
567
|
+
...('value' in descriptor ? getPreviewParts(descriptor.value, 2) : [['text', '(\u2026)']])
|
|
417
568
|
);
|
|
418
569
|
});
|
|
419
570
|
parts.push(['text', '}']);
|
|
@@ -501,30 +652,64 @@
|
|
|
501
652
|
// }
|
|
502
653
|
|
|
503
654
|
/**
|
|
504
|
-
* Runs
|
|
505
|
-
* shared between all of the code that is run.
|
|
655
|
+
* Runs a block of code.
|
|
506
656
|
* @param {string} jsCode
|
|
657
|
+
* @param {Object=} runOptions
|
|
658
|
+
* @param {"classic"|"module"=} runOptions.blockType
|
|
659
|
+
* "classic" (the default) runs the code as a classic script so that
|
|
660
|
+
* top-level declarations are shared between all of the code that is run.
|
|
661
|
+
* "module" runs the code as a module which allows for top-level await and
|
|
662
|
+
* import statements, but top-level declarations stay in the module.
|
|
663
|
+
* @param {[number, number]=} runOptions.resultRange
|
|
664
|
+
* The start and end index of the last expression in the code whose value
|
|
665
|
+
* should be shown.
|
|
507
666
|
*/
|
|
508
|
-
function runCode(jsCode) {
|
|
667
|
+
async function runCode(jsCode, runOptions) {
|
|
668
|
+
const {blockType, resultRange} = Object(runOptions);
|
|
669
|
+
|
|
509
670
|
// Names the code so that stack traces refer to it by this name.
|
|
510
|
-
const
|
|
671
|
+
const snippetName = `snippet-${++snippetCount}.js`;
|
|
511
672
|
|
|
512
|
-
|
|
513
|
-
|
|
673
|
+
// Wraps the last expression so that its value is reported. This keeps
|
|
674
|
+
// everything on the same lines so that line numbers in errors still match
|
|
675
|
+
// and the shift in columns is recorded so they can be fixed too.
|
|
676
|
+
if (Array.isArray(resultRange)) {
|
|
677
|
+
const [start, end] = resultRange;
|
|
678
|
+
const prefix = `globalThis[Symbol.for(${JSON.stringify(RESULT_KEY)})]((`;
|
|
679
|
+
const linesBefore = jsCode.slice(0, start).split('\n');
|
|
680
|
+
columnShiftsBySnippet[snippetName] = {
|
|
681
|
+
line: linesBefore.length,
|
|
682
|
+
column: linesBefore[linesBefore.length - 1].length + 1,
|
|
683
|
+
shift: prefix.length,
|
|
684
|
+
};
|
|
685
|
+
jsCode = jsCode.slice(0, start) + prefix + jsCode.slice(start, end) + '))' + jsCode.slice(end);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const source = `${jsCode}\n//# sourceURL=${snippetName}`;
|
|
689
|
+
|
|
690
|
+
runningBlockType = blockType === 'module' ? 'module' : 'classic';
|
|
691
|
+
try {
|
|
692
|
+
if (blockType === 'module') {
|
|
693
|
+
// Waits for the module to finish (including any top-level await).
|
|
694
|
+
await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
|
|
695
|
+
}
|
|
696
|
+
else if (mode === 'worker') {
|
|
514
697
|
importCode(source);
|
|
515
698
|
}
|
|
516
|
-
|
|
517
|
-
|
|
699
|
+
else {
|
|
700
|
+
const script = document.createElement('script');
|
|
701
|
+
script.textContent = source;
|
|
702
|
+
document.head.appendChild(script);
|
|
703
|
+
script.remove();
|
|
518
704
|
}
|
|
519
705
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
script.textContent = source;
|
|
523
|
-
document.head.appendChild(script);
|
|
524
|
-
script.remove();
|
|
706
|
+
catch (e) {
|
|
707
|
+
reportUncaught(e);
|
|
525
708
|
}
|
|
709
|
+
runningBlockType = undefined;
|
|
526
710
|
|
|
527
|
-
// Lets the viewer know that
|
|
711
|
+
// Lets the viewer know that the code finished running and that any logs
|
|
712
|
+
// made while it ran have already been sent.
|
|
528
713
|
send({target: 'viewer', func: 'onCodeRan', args: []});
|
|
529
714
|
}
|
|
530
715
|
|
|
@@ -592,6 +777,7 @@
|
|
|
592
777
|
*/
|
|
593
778
|
function clearLogs() {
|
|
594
779
|
for (const logId of Object.keys(logArgsById)) delete logArgsById[logId];
|
|
780
|
+
groupIds = [];
|
|
595
781
|
}
|
|
596
782
|
|
|
597
783
|
return {clearLogs, runCode, sendDescriptionFor};
|
|
@@ -705,10 +891,21 @@
|
|
|
705
891
|
function main(script) {
|
|
706
892
|
const dataset = JSON.parse(JSON.stringify(script.dataset));
|
|
707
893
|
const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
|
|
894
|
+
const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
|
|
895
|
+
const showResults = dataset.showResults !== 'false';
|
|
896
|
+
const libraryUrl = getLibraryFileUrl.bind(null, dataset.librariesUrl || DEFAULT_LIBRARIES_URL);
|
|
708
897
|
|
|
898
|
+
// The viewer in the page and, when the console is popped out, the viewer
|
|
899
|
+
// in the pop-out window. Messages from the runner go to the active one.
|
|
709
900
|
/** @type {ReturnType<createCallableFrame>} */
|
|
710
|
-
let
|
|
711
|
-
|
|
901
|
+
let inlineViewer;
|
|
902
|
+
/** @type {ReturnType<createCallableFrame>?} */
|
|
903
|
+
let popOutViewer = null;
|
|
904
|
+
/** @type {Window?} */
|
|
905
|
+
let popOutWindow = null;
|
|
906
|
+
let popOutWatcher;
|
|
907
|
+
let activeViewer;
|
|
908
|
+
const sendToViewer = data => relayMessage(data, {viewer: activeViewer});
|
|
712
909
|
const runner = runnerMode === 'window'
|
|
713
910
|
? createWindowRunner(sendToViewer)
|
|
714
911
|
: createWorkerRunner(sendToViewer);
|
|
@@ -718,1145 +915,1596 @@
|
|
|
718
915
|
? dataset.theme
|
|
719
916
|
: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
720
917
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const MIN_LOADING_TIME = 1000;
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* If the orientation isn't specified, the editor is shown below the console
|
|
736
|
-
* instead of beside it when the console is narrower than this (in pixels).
|
|
737
|
-
*/
|
|
738
|
-
const NARROW_WIDTH = 600;
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* @returns {"horizontal"|"vertical"}
|
|
742
|
-
* The orientation to use when one wasn't specified.
|
|
743
|
-
*/
|
|
744
|
-
function getAutoDividerOrient() {
|
|
745
|
-
return innerWidth < NARROW_WIDTH ? 'horizontal' : 'vertical';
|
|
918
|
+
// What the viewer can ask this script to do (see RELAYABLE_FUNCS.host).
|
|
919
|
+
const hostFuncs = {
|
|
920
|
+
/**
|
|
921
|
+
* Makes the IFRAME fill the window (used if full screen isn't allowed).
|
|
922
|
+
* @param {boolean} isMaximized
|
|
923
|
+
*/
|
|
924
|
+
setMaximized(isMaximized) {
|
|
925
|
+
const {style} = inlineViewer.iframe;
|
|
926
|
+
if (isMaximized) {
|
|
927
|
+
inlineViewer.normalCssText ??= style.cssText;
|
|
928
|
+
Object.assign(style, {position: 'fixed', inset: '0', width: '100%', height: '100%', zIndex: '2147483647'});
|
|
746
929
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
},
|
|
910
|
-
].filter(btn => !btn.showIf || btn.showIf.call(this));
|
|
911
|
-
},
|
|
912
|
-
canRunCode() {
|
|
913
|
-
return this.jsCode.trim();
|
|
914
|
-
},
|
|
915
|
-
mainElemClassNames() {
|
|
916
|
-
return [
|
|
917
|
-
this.dividerOrient === 'vertical' ? 'col-orient' : 'row-orient',
|
|
918
|
-
this.isMovingDivider ? 'is-moving-divider' : ''
|
|
919
|
-
].join(' ');
|
|
920
|
-
},
|
|
921
|
-
mainElemStyles() {
|
|
930
|
+
else if (inlineViewer.normalCssText != null) {
|
|
931
|
+
style.cssText = inlineViewer.normalCssText;
|
|
932
|
+
inlineViewer.normalCssText = null;
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
/**
|
|
936
|
+
* Opens the console in a separate window. The code still runs in this
|
|
937
|
+
* page.
|
|
938
|
+
* @param {*} state
|
|
939
|
+
* The state of the viewer (eg. what was logged) to show in the window.
|
|
940
|
+
*/
|
|
941
|
+
popOut(state) {
|
|
942
|
+
if (popOutWindow) {
|
|
943
|
+
popOutWindow.focus();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const rect = inlineViewer.iframe.getBoundingClientRect();
|
|
947
|
+
const width = Math.max(820, Math.round(rect.width));
|
|
948
|
+
const height = Math.max(560, Math.round(rect.height));
|
|
949
|
+
const newWindow = open('', '_blank', `popup,width=${width},height=${height}`);
|
|
950
|
+
if (!newWindow) {
|
|
951
|
+
inlineViewer.call('onPopOutFailed');
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
popOutWindow = newWindow;
|
|
955
|
+
popOutViewer = createViewer(newWindow, state);
|
|
956
|
+
// Lets the window send its state back even while it is closing (a
|
|
957
|
+
// message sent then can't be traced back to the window). The name
|
|
958
|
+
// must match POP_IN_FUNCTION_NAME in the viewer.
|
|
959
|
+
newWindow.yourjsBoxPopIn = state => newWindow === popOutWindow && hostFuncs.popIn(state);
|
|
960
|
+
activeViewer = popOutViewer;
|
|
961
|
+
inlineViewer.call('setPoppedOut', true);
|
|
962
|
+
|
|
963
|
+
// If the window is closed without sending its state (which it does
|
|
964
|
+
// when it is closed) bring the console back as it was.
|
|
965
|
+
popOutWatcher = setInterval(() => {
|
|
966
|
+
if (newWindow.closed) setTimeout(() => newWindow === popOutWindow && hostFuncs.popIn(null), 300);
|
|
967
|
+
}, 500);
|
|
968
|
+
},
|
|
969
|
+
/**
|
|
970
|
+
* Brings the console back into the page.
|
|
971
|
+
* @param {*} state
|
|
972
|
+
* The state of the pop-out window's viewer or `null` to keep the state
|
|
973
|
+
* from when the console was popped out.
|
|
974
|
+
*/
|
|
975
|
+
popIn(state) {
|
|
976
|
+
const oldWindow = popOutWindow;
|
|
977
|
+
if (!oldWindow) return;
|
|
978
|
+
clearInterval(popOutWatcher);
|
|
979
|
+
popOutWindow = null;
|
|
980
|
+
popOutViewer.dispose();
|
|
981
|
+
popOutViewer = null;
|
|
982
|
+
activeViewer = inlineViewer;
|
|
983
|
+
inlineViewer.call('setPoppedOut', false, state);
|
|
984
|
+
if (!oldWindow.closed) oldWindow.close();
|
|
985
|
+
},
|
|
986
|
+
/** Asks the pop-out window to send its state back to the page. */
|
|
987
|
+
requestPopIn() {
|
|
988
|
+
if (popOutViewer) popOutViewer.call('popIn');
|
|
989
|
+
},
|
|
990
|
+
focusPopOut() {
|
|
991
|
+
popOutWindow?.focus();
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// The pop-out window can't work without this page so it is closed too.
|
|
996
|
+
addEventListener('pagehide', () => popOutWindow?.close());
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* @param {Window=} targetWindow
|
|
1000
|
+
* If given the viewer is written into this window (a pop-out window).
|
|
1001
|
+
* @param {*=} state
|
|
1002
|
+
* The state to restore in the pop-out window.
|
|
1003
|
+
* @returns {ReturnType<createCallableFrame>}
|
|
1004
|
+
*/
|
|
1005
|
+
function createViewer(targetWindow, state) {
|
|
1006
|
+
return createCallableFrame({
|
|
1007
|
+
jsCode() {
|
|
1008
|
+
let mountedApp;
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Indicates if any of the libraries that the viewer needs failed to load (eg.
|
|
1012
|
+
* because the CDN is blocked or the user is offline).
|
|
1013
|
+
*/
|
|
1014
|
+
const IS_MISSING_LIBRARIES = !window.Vue || !window.ace || !window.Prism;
|
|
1015
|
+
if (IS_MISSING_LIBRARIES) {
|
|
1016
|
+
document.querySelector('#splash').classList.add('failed');
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const Prism = window.Prism;
|
|
1020
|
+
delete window.Prism;
|
|
1021
|
+
Prism?.plugins.autoloader.loadLanguages('javascript');
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* The minimum amount of time (in milliseconds) that the loading screen is shown.
|
|
1025
|
+
*/
|
|
1026
|
+
const MIN_LOADING_TIME = 1000;
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* The name of the function that the page adds to a pop-out window so that the
|
|
1030
|
+
* window can send its state back to the page (see bringBack()).
|
|
1031
|
+
*/
|
|
1032
|
+
const POP_IN_FUNCTION_NAME = 'yourjsBoxPopIn';
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* The text sizes (as a multiple of the normal size) that can be chosen.
|
|
1036
|
+
*/
|
|
1037
|
+
const TEXT_SCALES = [0.75, 0.9, 1, 1.15, 1.3, 1.5, 1.75, 2];
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Where the chosen text size is remembered. It applies to every console on
|
|
1041
|
+
* the same site.
|
|
1042
|
+
*/
|
|
1043
|
+
const TEXT_SCALE_STORAGE_KEY = 'yourjs-box.textScale';
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* @returns {number}
|
|
1047
|
+
* The remembered text size or `1` if there isn't one.
|
|
1048
|
+
*/
|
|
1049
|
+
function loadTextScale() {
|
|
1050
|
+
try {
|
|
1051
|
+
const textScale = +localStorage.getItem(TEXT_SCALE_STORAGE_KEY);
|
|
1052
|
+
return TEXT_SCALES.includes(textScale) ? textScale : 1;
|
|
1053
|
+
}
|
|
1054
|
+
catch (e) {
|
|
1055
|
+
return 1;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* If the orientation isn't specified, the editor is shown below the console
|
|
1061
|
+
* instead of beside it when the console is narrower than this (in pixels).
|
|
1062
|
+
*/
|
|
1063
|
+
const NARROW_WIDTH = 600;
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* @returns {"horizontal"|"vertical"}
|
|
1067
|
+
* The orientation to use when one wasn't specified.
|
|
1068
|
+
*/
|
|
1069
|
+
function getAutoDividerOrient() {
|
|
1070
|
+
return innerWidth < NARROW_WIDTH ? 'horizontal' : 'vertical';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* @param {string} jsCode
|
|
1075
|
+
* @param {{[key: string]: string}} dataset
|
|
1076
|
+
* The data attributes exactly as they were specified on the script tag.
|
|
1077
|
+
* @param {{runnerMode: "worker"|"window", packageInfo: {name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}} meta
|
|
1078
|
+
*/
|
|
1079
|
+
function init(jsCode, dataset, meta) {
|
|
1080
|
+
if (IS_MISSING_LIBRARIES) return;
|
|
1081
|
+
|
|
1082
|
+
const hidePrefix = dataset.hidePrefix ?? '';
|
|
1083
|
+
const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
|
|
1084
|
+
const originalCode = unindentMin(jsCode);
|
|
1085
|
+
const {visibleCode, hiddenGroups} = extractHiddenGroups(originalCode, hidePrefix);
|
|
1086
|
+
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
|
|
1087
|
+
const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
|
|
1088
|
+
|
|
1089
|
+
mountedApp = Vue
|
|
1090
|
+
.createApp({
|
|
1091
|
+
data() {
|
|
922
1092
|
return {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1093
|
+
displays: [],
|
|
1094
|
+
hidePrefix,
|
|
1095
|
+
hiddenGroups: copyHiddenGroups(),
|
|
1096
|
+
runnerMode: meta.runnerMode,
|
|
1097
|
+
blockType: meta.blockType,
|
|
1098
|
+
showResults: meta.showResults,
|
|
1099
|
+
packageInfo: meta.packageInfo,
|
|
1100
|
+
libraryVersions: meta.libraryVersions,
|
|
1101
|
+
// Every group of code that was run (in order) which is used when
|
|
1102
|
+
// copying the console as HTML. Unlike the displays this is only
|
|
1103
|
+
// cleared by a reset.
|
|
1104
|
+
runHistory: [],
|
|
1105
|
+
isAboutOpen: false,
|
|
1106
|
+
aboutTab: 'about',
|
|
1107
|
+
exportCode: 'current',
|
|
1108
|
+
exportFormat: 'snippet',
|
|
1109
|
+
copyLabel: 'Copy',
|
|
1110
|
+
// The number of groups of code sent to the runner that haven't
|
|
1111
|
+
// finished running yet.
|
|
1112
|
+
runningCount: 0,
|
|
1113
|
+
/** @type {{title: string, message: string, confirmText: string, resolve: (isConfirmed: boolean) => void}?} */
|
|
1114
|
+
dialog: null,
|
|
1115
|
+
isDisplaysScrolledToBottom: true,
|
|
1116
|
+
// Like the browser's dev tools, follow the system's color scheme
|
|
1117
|
+
// unless a theme was specified.
|
|
1118
|
+
forcedTheme: /^(light|dark)$/.test(dataset.theme) ? dataset.theme : null,
|
|
1119
|
+
prefersDark: darkSchemeQuery.matches,
|
|
1120
|
+
runCount: 0,
|
|
1121
|
+
jsCode: visibleCode,
|
|
1122
|
+
// If the orientation wasn't specified it follows the width of the
|
|
1123
|
+
// console until the user chooses one.
|
|
1124
|
+
isDividerOrientAuto: !/^(horizontal|vertical)$/.test(dataset.dividerOrient),
|
|
1125
|
+
dividerOrient: /^(horizontal|vertical)$/.test(dataset.dividerOrient)
|
|
1126
|
+
? dataset.dividerOrient
|
|
1127
|
+
: getAutoDividerOrient(),
|
|
1128
|
+
isMovingDivider: false,
|
|
1129
|
+
dividerPct: '50%',
|
|
1130
|
+
dividerSize: '8px',
|
|
1131
|
+
tempDividerPct: null,
|
|
1132
|
+
isMenuOpen: false,
|
|
1133
|
+
textScale: loadTextScale(),
|
|
1134
|
+
isFullscreen: false,
|
|
1135
|
+
// Set when full screen isn't allowed so the console fills the page.
|
|
1136
|
+
isMaximized: false,
|
|
1137
|
+
// Indicates if this is the viewer in a pop-out window.
|
|
1138
|
+
isPopOut: meta.isPopOut,
|
|
1139
|
+
// Indicates if this viewer (in the page) has been popped out.
|
|
1140
|
+
isPoppedOut: false,
|
|
1141
|
+
// When popped out, the state of the console that was in the page.
|
|
1142
|
+
...(meta.popOutState ?? {}),
|
|
926
1143
|
};
|
|
927
|
-
}
|
|
928
|
-
},
|
|
929
|
-
methods: {
|
|
930
|
-
/**
|
|
931
|
-
* @param {KeyboardEvent} evt
|
|
932
|
-
*/
|
|
933
|
-
onEditorKeyCombo(evt) {
|
|
934
|
-
if ((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter' && this.canRunCode) {
|
|
935
|
-
this.runCode();
|
|
936
|
-
}
|
|
937
1144
|
},
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1145
|
+
computed: {
|
|
1146
|
+
theme() {
|
|
1147
|
+
return this.forcedTheme ?? (this.prefersDark ? 'dark' : 'light');
|
|
1148
|
+
},
|
|
1149
|
+
modKey() {
|
|
1150
|
+
return isMac ? '\u2318' : 'Ctrl';
|
|
1151
|
+
},
|
|
1152
|
+
runnerDescription() {
|
|
1153
|
+
return this.runnerMode === 'window'
|
|
1154
|
+
? 'This page (it can use the page\'s globals and DOM)'
|
|
1155
|
+
: 'A Web Worker (isolated from the page, no DOM)';
|
|
1156
|
+
},
|
|
1157
|
+
themeDescription() {
|
|
1158
|
+
const name = this.theme === 'dark' ? 'Dark' : 'Light';
|
|
1159
|
+
return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
|
|
1160
|
+
},
|
|
1161
|
+
blockTypeDescription() {
|
|
1162
|
+
return this.blockType === 'module'
|
|
1163
|
+
? 'Modules (top-level await and imports work, declarations stay in each block)'
|
|
1164
|
+
: 'Classic scripts (top-level declarations are shared between blocks)';
|
|
1165
|
+
},
|
|
1166
|
+
/**
|
|
1167
|
+
* The IDs of the groups (from console.groupCollapsed() or a group that
|
|
1168
|
+
* the user collapsed) whose messages should be hidden.
|
|
1169
|
+
*/
|
|
1170
|
+
collapsedGroupIds() {
|
|
1171
|
+
return new Set(
|
|
1172
|
+
this.displays
|
|
1173
|
+
.filter(d => d.groupId && d.isCollapsed)
|
|
1174
|
+
.map(d => d.groupId)
|
|
1175
|
+
);
|
|
1176
|
+
},
|
|
1177
|
+
layoutDescription() {
|
|
1178
|
+
return (this.dividerOrient === 'vertical' ? 'Editor beside the console' : 'Editor below the console')
|
|
1179
|
+
+ (this.isDividerOrientAuto ? ' (automatic)' : '');
|
|
1180
|
+
},
|
|
1181
|
+
exportCodeOptions() {
|
|
1182
|
+
return [
|
|
1183
|
+
{value: 'current', label: 'Current code', note: 'The code that already ran followed by the code in the editor.'},
|
|
1184
|
+
{value: 'original', label: 'Original code', note: 'The code this console started with.'},
|
|
1185
|
+
{value: 'blank', label: 'Blank', note: 'An empty console with the same settings.'},
|
|
1186
|
+
];
|
|
1187
|
+
},
|
|
1188
|
+
exportFormatOptions() {
|
|
1189
|
+
return [
|
|
1190
|
+
{value: 'snippet', label: 'Embed snippet'},
|
|
1191
|
+
{value: 'page', label: 'Full page'},
|
|
1192
|
+
];
|
|
1193
|
+
},
|
|
1194
|
+
exportNote() {
|
|
1195
|
+
return this.exportCodeOptions.find(o => o.value === this.exportCode).note;
|
|
1196
|
+
},
|
|
1197
|
+
/**
|
|
1198
|
+
* The code that the copied console will start with.
|
|
1199
|
+
*/
|
|
1200
|
+
exportJsCode() {
|
|
1201
|
+
if (this.exportCode === 'original') return originalCode;
|
|
1202
|
+
if (this.exportCode === 'blank') return '';
|
|
1203
|
+
|
|
1204
|
+
// The groups that already ran, then the groups in the editor with
|
|
1205
|
+
// any hidden groups that haven't run yet in their original places.
|
|
1206
|
+
const blocks = this.runHistory.slice();
|
|
1207
|
+
const pendingHiddenGroups = this.hiddenGroups.slice();
|
|
1208
|
+
let visibleCount = this.runCount;
|
|
1209
|
+
const addPendingHiddenGroups = () => {
|
|
1210
|
+
while (pendingHiddenGroups.length && pendingHiddenGroups[0].runCount <= visibleCount) {
|
|
1211
|
+
blocks.push(pendingHiddenGroups.shift().allLines);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
addPendingHiddenGroups();
|
|
1215
|
+
for (const group of parseJSCodeGroups(this.jsCode)) {
|
|
1216
|
+
if (!group.allLines.trim()) continue;
|
|
1217
|
+
blocks.push(group.allLines);
|
|
1218
|
+
visibleCount++;
|
|
1219
|
+
addPendingHiddenGroups();
|
|
1220
|
+
}
|
|
1221
|
+
blocks.push(...pendingHiddenGroups.map(group => group.allLines));
|
|
1222
|
+
return joinCodeBlocks(blocks);
|
|
1223
|
+
},
|
|
1224
|
+
/**
|
|
1225
|
+
* The data attributes for the copied console. data-libraries-url is
|
|
1226
|
+
* left out because it usually points to files on this site while the
|
|
1227
|
+
* copied console loads JS Box from a CDN.
|
|
1228
|
+
*/
|
|
1229
|
+
exportDataset() {
|
|
1230
|
+
const {librariesUrl, ...exportDataset} = dataset;
|
|
1231
|
+
return exportDataset;
|
|
1232
|
+
},
|
|
1233
|
+
exportHtml() {
|
|
1234
|
+
return buildConsoleHtml({
|
|
1235
|
+
code: this.exportJsCode,
|
|
1236
|
+
dataset: this.exportDataset,
|
|
1237
|
+
packageInfo: this.packageInfo,
|
|
1238
|
+
isFullPage: this.exportFormat === 'page',
|
|
1239
|
+
});
|
|
1240
|
+
},
|
|
1241
|
+
bottomButtons() {
|
|
1242
|
+
const isFullscreen = this.isFullscreen || this.isMaximized;
|
|
1243
|
+
return [
|
|
1244
|
+
{
|
|
1245
|
+
iconName: 'clear',
|
|
1246
|
+
title: 'Clear console',
|
|
1247
|
+
callback() { this.clearConsole(); },
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
iconName: isFullscreen ? 'exitFullscreen' : 'fullscreen',
|
|
1251
|
+
title: isFullscreen ? 'Exit full screen' : 'Full screen',
|
|
1252
|
+
callback() { this.toggleFullscreen(); },
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
iconName: 'more',
|
|
1256
|
+
title: 'More',
|
|
1257
|
+
className: 'more-button',
|
|
1258
|
+
callback() { this.toggleMenu(); },
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
iconName: this.runningCount ? 'spinner' : 'play',
|
|
1262
|
+
label: 'Run',
|
|
1263
|
+
className: 'primary',
|
|
1264
|
+
title: `Run the next block of code (${isMac ? '\u2318' : 'Ctrl+'}Enter)`,
|
|
1265
|
+
callback() { this.runCode(); },
|
|
1266
|
+
disableIf() { return !this.canRunCode; }
|
|
1267
|
+
},
|
|
1268
|
+
];
|
|
1269
|
+
},
|
|
1270
|
+
canShrinkText() {
|
|
1271
|
+
return this.textScale > TEXT_SCALES[0];
|
|
1272
|
+
},
|
|
1273
|
+
canGrowText() {
|
|
1274
|
+
return this.textScale < TEXT_SCALES[TEXT_SCALES.length - 1];
|
|
1275
|
+
},
|
|
1276
|
+
canRunCode() {
|
|
1277
|
+
return this.jsCode.trim();
|
|
1278
|
+
},
|
|
1279
|
+
mainElemClassNames() {
|
|
1280
|
+
return [
|
|
1281
|
+
this.dividerOrient === 'vertical' ? 'col-orient' : 'row-orient',
|
|
1282
|
+
this.isMovingDivider ? 'is-moving-divider' : ''
|
|
1283
|
+
].join(' ');
|
|
1284
|
+
},
|
|
1285
|
+
mainElemStyles() {
|
|
1286
|
+
return {
|
|
1287
|
+
'--divider-size': this.dividerSize,
|
|
1288
|
+
'--editor-pct': this.dividerPct,
|
|
1289
|
+
'--temp-editor-pct': this.tempDividerPct
|
|
1290
|
+
};
|
|
959
1291
|
}
|
|
960
1292
|
},
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1293
|
+
methods: {
|
|
1294
|
+
/**
|
|
1295
|
+
* @param {KeyboardEvent} evt
|
|
1296
|
+
*/
|
|
1297
|
+
onEditorKeyCombo(evt) {
|
|
1298
|
+
if ((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter' && this.canRunCode) {
|
|
1299
|
+
this.runCode();
|
|
1300
|
+
}
|
|
1301
|
+
},
|
|
1302
|
+
runCode() {
|
|
1303
|
+
// Always show the output of code that is run.
|
|
1304
|
+
this.isDisplaysScrolledToBottom = true;
|
|
1305
|
+
|
|
1306
|
+
const [jsCodeGroup0, ...otherJsCodeGroups] = parseJSCodeGroups(this.jsCode);
|
|
1307
|
+
this.runGroup(jsCodeGroup0);
|
|
1308
|
+
|
|
1309
|
+
// Remove the code that was run from the editor.
|
|
1310
|
+
this.jsCode = otherJsCodeGroups.map(g => g.allLines).join('\n');
|
|
1311
|
+
|
|
1312
|
+
this.runCount++;
|
|
1313
|
+
},
|
|
1314
|
+
/**
|
|
1315
|
+
* Runs the next hidden group if all of the visible code that came
|
|
1316
|
+
* before it has already been run. This is called again once the runner
|
|
1317
|
+
* finishes running each group so that the displays stay in order.
|
|
1318
|
+
*/
|
|
1319
|
+
runHiddenGroups() {
|
|
1320
|
+
const {hiddenGroups} = this;
|
|
1321
|
+
if (hiddenGroups.length && hiddenGroups[0].runCount <= this.runCount) {
|
|
1322
|
+
this.runGroup(hiddenGroups.shift());
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
/**
|
|
1326
|
+
* Adds a group of code to the displays and then runs it.
|
|
1327
|
+
* @param {ReturnType<parseJSCodeGroups>[number]} group
|
|
1328
|
+
*/
|
|
1329
|
+
runGroup(group) {
|
|
1330
|
+
const {header, isHidden} = parseGroupHeader(group.headerLines, this.hidePrefix);
|
|
1331
|
+
this.displays.push({
|
|
1332
|
+
type: 'prism',
|
|
1333
|
+
header,
|
|
1334
|
+
isHidden,
|
|
1335
|
+
isCodeShown: !isHidden,
|
|
1336
|
+
value: group.lines,
|
|
1337
|
+
});
|
|
1338
|
+
this.runningCount++;
|
|
1339
|
+
this.runHistory.push(group.allLines);
|
|
1340
|
+
messageParent({
|
|
1341
|
+
target: 'runner',
|
|
1342
|
+
func: 'runCode',
|
|
1343
|
+
args: [group.lines, {
|
|
1344
|
+
blockType: this.blockType,
|
|
1345
|
+
// Like the browser's console, show the value of the last
|
|
1346
|
+
// expression (except for hidden code).
|
|
1347
|
+
resultRange: this.showResults && !isHidden
|
|
1348
|
+
? findLastExpression(group.lines, this.blockType)
|
|
1349
|
+
: null,
|
|
1350
|
+
}],
|
|
1351
|
+
});
|
|
1352
|
+
},
|
|
1353
|
+
clearConsole() {
|
|
1354
|
+
this.displays = [];
|
|
1355
|
+
messageParent({target: 'runner', func: 'clearLogs', args: []});
|
|
1356
|
+
},
|
|
1357
|
+
async resetConsole() {
|
|
1010
1358
|
const isConfirmed = await this.confirm({
|
|
1011
|
-
title: '
|
|
1012
|
-
message: 'The
|
|
1013
|
-
|
|
1359
|
+
title: 'Reset the console?',
|
|
1360
|
+
message: 'The output will be cleared and the editor will go back to the original code.'
|
|
1361
|
+
+ (this.runnerMode === 'window'
|
|
1362
|
+
? ' Anything the code already defined on the page will stay defined.'
|
|
1363
|
+
: ''),
|
|
1364
|
+
confirmText: 'Reset',
|
|
1014
1365
|
});
|
|
1015
1366
|
if (!isConfirmed) return;
|
|
1367
|
+
|
|
1368
|
+
this.displays = [];
|
|
1369
|
+
this.jsCode = visibleCode;
|
|
1370
|
+
this.hiddenGroups = copyHiddenGroups();
|
|
1371
|
+
this.runCount = 0;
|
|
1372
|
+
this.runningCount = 0;
|
|
1373
|
+
this.runHistory = [];
|
|
1374
|
+
messageParent({target: 'runner', func: 'reset', args: []});
|
|
1375
|
+
this.runHiddenGroups();
|
|
1376
|
+
},
|
|
1377
|
+
/**
|
|
1378
|
+
* Copies code that was already run into the editor so that it can be run
|
|
1379
|
+
* again (as is or modified), first asking before replacing any code that
|
|
1380
|
+
* is in the editor.
|
|
1381
|
+
* @param {string} code
|
|
1382
|
+
*/
|
|
1383
|
+
async copyToEditor(code) {
|
|
1384
|
+
if (this.jsCode.trim() && this.jsCode !== code) {
|
|
1385
|
+
const isConfirmed = await this.confirm({
|
|
1386
|
+
title: 'Replace the code in the editor?',
|
|
1387
|
+
message: 'The code that is currently in the editor will be replaced with a copy of the code you selected.',
|
|
1388
|
+
confirmText: 'Replace',
|
|
1389
|
+
});
|
|
1390
|
+
if (!isConfirmed) return;
|
|
1391
|
+
}
|
|
1392
|
+
this.jsCode = code;
|
|
1393
|
+
},
|
|
1394
|
+
/**
|
|
1395
|
+
* Shows a dialog asking the user to confirm something.
|
|
1396
|
+
* @param {{title: string, message: string, confirmText: string}} options
|
|
1397
|
+
* @returns {Promise<boolean>}
|
|
1398
|
+
*/
|
|
1399
|
+
confirm(options) {
|
|
1400
|
+
this.dialog?.resolve(false);
|
|
1401
|
+
return new Promise(resolve => {
|
|
1402
|
+
this.dialog = {...options, resolve};
|
|
1403
|
+
this.$nextTick(() => this.$refs.dialogConfirmButton?.focus());
|
|
1404
|
+
});
|
|
1405
|
+
},
|
|
1406
|
+
/**
|
|
1407
|
+
* @param {"about"|"html"} tab
|
|
1408
|
+
*/
|
|
1409
|
+
openAbout(tab) {
|
|
1410
|
+
this.aboutTab = tab;
|
|
1411
|
+
this.isAboutOpen = true;
|
|
1412
|
+
this.$nextTick(() => this.$refs.aboutCloseButton?.focus());
|
|
1413
|
+
},
|
|
1414
|
+
closeAbout() {
|
|
1415
|
+
this.isAboutOpen = false;
|
|
1416
|
+
},
|
|
1417
|
+
async copyExport() {
|
|
1418
|
+
const text = this.exportHtml;
|
|
1419
|
+
try {
|
|
1420
|
+
await navigator.clipboard.writeText(text);
|
|
1421
|
+
}
|
|
1422
|
+
catch (e) {
|
|
1423
|
+
// Falls back to the older way of copying text.
|
|
1424
|
+
const textarea = Object.assign(document.createElement('textarea'), {value: text});
|
|
1425
|
+
document.body.appendChild(textarea);
|
|
1426
|
+
textarea.select();
|
|
1427
|
+
document.execCommand('copy');
|
|
1428
|
+
textarea.remove();
|
|
1429
|
+
}
|
|
1430
|
+
this.copyLabel = 'Copied!';
|
|
1431
|
+
clearTimeout(this.copyLabelTimeout);
|
|
1432
|
+
this.copyLabelTimeout = setTimeout(() => this.copyLabel = 'Copy', 1600);
|
|
1433
|
+
},
|
|
1434
|
+
/**
|
|
1435
|
+
* Downloads the console as a full HTML page.
|
|
1436
|
+
*/
|
|
1437
|
+
downloadExport() {
|
|
1438
|
+
const html = buildConsoleHtml({
|
|
1439
|
+
code: this.exportJsCode,
|
|
1440
|
+
dataset: this.exportDataset,
|
|
1441
|
+
packageInfo: this.packageInfo,
|
|
1442
|
+
isFullPage: true,
|
|
1443
|
+
});
|
|
1444
|
+
const url = URL.createObjectURL(new Blob([html], {type: 'text/html'}));
|
|
1445
|
+
Object.assign(document.createElement('a'), {href: url, download: 'js-box.html'}).click();
|
|
1446
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1447
|
+
},
|
|
1448
|
+
/**
|
|
1449
|
+
* Clicking a group's header (from console.group()) collapses or expands
|
|
1450
|
+
* the group unless a value in the header was clicked.
|
|
1451
|
+
* @param {*} display
|
|
1452
|
+
* @param {MouseEvent} evt
|
|
1453
|
+
*/
|
|
1454
|
+
onLogRowClick(display, evt) {
|
|
1455
|
+
if (display.groupId && !evt.target.closest('.js-value-header.expandable')) {
|
|
1456
|
+
display.isCollapsed = !display.isCollapsed;
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
/**
|
|
1460
|
+
* Indicates if a display is inside of a collapsed group.
|
|
1461
|
+
*/
|
|
1462
|
+
isInCollapsedGroup(display) {
|
|
1463
|
+
const {collapsedGroupIds} = this;
|
|
1464
|
+
return !!collapsedGroupIds.size && !!display.groupIds?.some(id => collapsedGroupIds.has(id));
|
|
1465
|
+
},
|
|
1466
|
+
/**
|
|
1467
|
+
* @returns {*}
|
|
1468
|
+
* A copy of the parts of the console's state that are moved between
|
|
1469
|
+
* the page and a pop-out window.
|
|
1470
|
+
*/
|
|
1471
|
+
getState() {
|
|
1472
|
+
return JSON.parse(JSON.stringify({
|
|
1473
|
+
displays: this.displays,
|
|
1474
|
+
jsCode: this.jsCode,
|
|
1475
|
+
runHistory: this.runHistory,
|
|
1476
|
+
hiddenGroups: this.hiddenGroups,
|
|
1477
|
+
runCount: this.runCount,
|
|
1478
|
+
runningCount: this.runningCount,
|
|
1479
|
+
dividerOrient: this.dividerOrient,
|
|
1480
|
+
isDividerOrientAuto: this.isDividerOrientAuto,
|
|
1481
|
+
dividerPct: this.dividerPct,
|
|
1482
|
+
}));
|
|
1483
|
+
},
|
|
1484
|
+
/**
|
|
1485
|
+
* @param {ReturnType<this['getState']>} state
|
|
1486
|
+
*/
|
|
1487
|
+
restoreState(state) {
|
|
1488
|
+
Object.assign(this, state);
|
|
1489
|
+
this.isDisplaysScrolledToBottom = true;
|
|
1490
|
+
},
|
|
1491
|
+
toggleMenu() {
|
|
1492
|
+
if (this.isMenuOpen) {
|
|
1493
|
+
this.closeMenu();
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
this.isMenuOpen = true;
|
|
1497
|
+
this.$nextTick(() => this.$refs.moreMenu?.querySelector('.menu-item:not(:disabled)')?.focus());
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
closeMenu(shouldFocusButton) {
|
|
1501
|
+
this.isMenuOpen = false;
|
|
1502
|
+
if (shouldFocusButton) document.querySelector('#bottomNav .more-button')?.focus();
|
|
1503
|
+
},
|
|
1504
|
+
/**
|
|
1505
|
+
* Lets the arrow keys move between the menu's items.
|
|
1506
|
+
* @param {KeyboardEvent} evt
|
|
1507
|
+
*/
|
|
1508
|
+
onMenuKeyDown(evt) {
|
|
1509
|
+
if (evt.key === 'Escape') {
|
|
1510
|
+
evt.stopPropagation();
|
|
1511
|
+
this.closeMenu(true);
|
|
1512
|
+
}
|
|
1513
|
+
else if (evt.key === 'ArrowDown' || evt.key === 'ArrowUp') {
|
|
1514
|
+
evt.preventDefault();
|
|
1515
|
+
const items = [...this.$refs.moreMenu.querySelectorAll('.menu-item:not(:disabled)')];
|
|
1516
|
+
const index = items.indexOf(document.activeElement);
|
|
1517
|
+
const next = items[(index + (evt.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length];
|
|
1518
|
+
next?.focus();
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
/**
|
|
1522
|
+
* @param {number} direction
|
|
1523
|
+
* `1` to make the text bigger or `-1` to make it smaller.
|
|
1524
|
+
*/
|
|
1525
|
+
changeTextScale(direction) {
|
|
1526
|
+
const index = TEXT_SCALES.indexOf(this.textScale) + direction;
|
|
1527
|
+
if (index >= 0 && index < TEXT_SCALES.length) this.setTextScale(TEXT_SCALES[index]);
|
|
1528
|
+
},
|
|
1529
|
+
setTextScale(textScale) {
|
|
1530
|
+
this.textScale = textScale;
|
|
1531
|
+
try {
|
|
1532
|
+
localStorage.setItem(TEXT_SCALE_STORAGE_KEY, `${textScale}`);
|
|
1533
|
+
}
|
|
1534
|
+
catch (e) {}
|
|
1535
|
+
},
|
|
1536
|
+
/**
|
|
1537
|
+
* Uses the browser's full screen if it's allowed, otherwise the
|
|
1538
|
+
* console is made to fill the page.
|
|
1539
|
+
*/
|
|
1540
|
+
async toggleFullscreen() {
|
|
1541
|
+
if (this.isMaximized) {
|
|
1542
|
+
this.setMaximized(false);
|
|
1543
|
+
}
|
|
1544
|
+
else if (document.fullscreenElement) {
|
|
1545
|
+
await document.exitFullscreen();
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
try {
|
|
1549
|
+
await document.documentElement.requestFullscreen();
|
|
1550
|
+
}
|
|
1551
|
+
catch (e) {
|
|
1552
|
+
// A pop-out window can't be made to fill anything else.
|
|
1553
|
+
if (!this.isPopOut) this.setMaximized(true);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
setMaximized(isMaximized) {
|
|
1558
|
+
this.isMaximized = isMaximized;
|
|
1559
|
+
messageParent({target: 'host', func: 'setMaximized', args: [isMaximized]});
|
|
1560
|
+
},
|
|
1561
|
+
/**
|
|
1562
|
+
* Moves the console into a separate window. The code still runs in
|
|
1563
|
+
* the page.
|
|
1564
|
+
*/
|
|
1565
|
+
popOut() {
|
|
1566
|
+
this.closeMenu();
|
|
1567
|
+
if (this.isMaximized) this.setMaximized(false);
|
|
1568
|
+
messageParent({target: 'host', func: 'popOut', args: [this.getState()]});
|
|
1569
|
+
},
|
|
1570
|
+
/**
|
|
1571
|
+
* Moves the console from the pop-out window back into the page.
|
|
1572
|
+
*/
|
|
1573
|
+
bringBack() {
|
|
1574
|
+
this.hasSentState = true;
|
|
1575
|
+
// The page provides a function to call directly because a message
|
|
1576
|
+
// sent while this window is closing can't be traced back to it.
|
|
1577
|
+
const popIn = window[POP_IN_FUNCTION_NAME];
|
|
1578
|
+
if ('function' === typeof popIn) popIn(this.getState());
|
|
1579
|
+
else messageParent({target: 'host', func: 'popIn', args: [this.getState()]});
|
|
1580
|
+
},
|
|
1581
|
+
focusPopOut() {
|
|
1582
|
+
messageParent({target: 'host', func: 'focusPopOut', args: []});
|
|
1583
|
+
},
|
|
1584
|
+
requestPopIn() {
|
|
1585
|
+
messageParent({target: 'host', func: 'requestPopIn', args: []});
|
|
1586
|
+
},
|
|
1587
|
+
/**
|
|
1588
|
+
* Closes the menu when clicking outside of it.
|
|
1589
|
+
* @param {MouseEvent} evt
|
|
1590
|
+
*/
|
|
1591
|
+
onWindowMouseDown(evt) {
|
|
1592
|
+
if (this.isMenuOpen && !evt.target.closest('.more-menu, .more-button')) {
|
|
1593
|
+
this.closeMenu();
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
/**
|
|
1597
|
+
* @param {KeyboardEvent} evt
|
|
1598
|
+
*/
|
|
1599
|
+
onWindowKeyDown(evt) {
|
|
1600
|
+
if (evt.key === 'Escape' && this.isMaximized && !this.isMenuOpen && !this.dialog && !this.isAboutOpen) {
|
|
1601
|
+
this.setMaximized(false);
|
|
1602
|
+
}
|
|
1603
|
+
},
|
|
1604
|
+
closeDialog(isConfirmed) {
|
|
1605
|
+
const {dialog} = this;
|
|
1606
|
+
this.dialog = null;
|
|
1607
|
+
dialog?.resolve(isConfirmed);
|
|
1608
|
+
},
|
|
1609
|
+
/**
|
|
1610
|
+
* Sets the orientation chosen by the user which stops it from
|
|
1611
|
+
* automatically following the width of the console.
|
|
1612
|
+
* @param {"horizontal"|"vertical"} orient
|
|
1613
|
+
*/
|
|
1614
|
+
setDividerOrient(orient) {
|
|
1615
|
+
this.isDividerOrientAuto = false;
|
|
1616
|
+
this.dividerOrient = orient;
|
|
1617
|
+
},
|
|
1618
|
+
onWindowResize() {
|
|
1619
|
+
if (this.isDividerOrientAuto) {
|
|
1620
|
+
this.dividerOrient = getAutoDividerOrient();
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
onDisplaysScroll() {
|
|
1624
|
+
const {scrollTop, scrollHeight, clientHeight} = this.$refs.displaysScroller;
|
|
1625
|
+
this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
|
|
1626
|
+
},
|
|
1627
|
+
getEditorPct(evt) {
|
|
1628
|
+
const rect = this.$refs.main.getBoundingClientRect();
|
|
1629
|
+
const {dividerOrient} = this;
|
|
1630
|
+
const x = evt.pageX - rect.left;
|
|
1631
|
+
const y = evt.pageY - rect.top;
|
|
1632
|
+
const w = rect.width;
|
|
1633
|
+
const h = rect.height;
|
|
1634
|
+
const pct = 100 - 100 * Math.min(
|
|
1635
|
+
Math.max(
|
|
1636
|
+
0.2,
|
|
1637
|
+
dividerOrient === 'vertical' ? x / w : (y / h)
|
|
1638
|
+
),
|
|
1639
|
+
0.8
|
|
1640
|
+
);
|
|
1641
|
+
return pct + '%';
|
|
1642
|
+
},
|
|
1643
|
+
onMainElemMouseDown(evt) {
|
|
1644
|
+
if (evt.target === this.$refs.mainDivider) {
|
|
1645
|
+
this.isMovingDivider = true;
|
|
1646
|
+
this.tempDividerPct = this.getEditorPct(evt);
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
onWindowMouseMove(evt) {
|
|
1650
|
+
if (this.isMovingDivider) {
|
|
1651
|
+
this.tempDividerPct = this.getEditorPct(evt);
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
onWindowMouseUp(evt) {
|
|
1655
|
+
if (this.isMovingDivider) {
|
|
1656
|
+
this.isMovingDivider = false;
|
|
1657
|
+
this.dividerPct = this.getEditorPct(evt);
|
|
1658
|
+
}
|
|
1659
|
+
},
|
|
1660
|
+
onWindowError(evt) {
|
|
1661
|
+
this.displays.push({
|
|
1662
|
+
type: 'error',
|
|
1663
|
+
message: evt.error?.stack ?? evt.message ?? evt.error?.message ?? `${evt.error}`,
|
|
1664
|
+
line: evt.lineno,
|
|
1665
|
+
column: evt.colno,
|
|
1666
|
+
})
|
|
1016
1667
|
}
|
|
1017
|
-
this.jsCode = code;
|
|
1018
|
-
},
|
|
1019
|
-
/**
|
|
1020
|
-
* Shows a dialog asking the user to confirm something.
|
|
1021
|
-
* @param {{title: string, message: string, confirmText: string}} options
|
|
1022
|
-
* @returns {Promise<boolean>}
|
|
1023
|
-
*/
|
|
1024
|
-
confirm(options) {
|
|
1025
|
-
this.dialog?.resolve(false);
|
|
1026
|
-
return new Promise(resolve => {
|
|
1027
|
-
this.dialog = {...options, resolve};
|
|
1028
|
-
this.$nextTick(() => this.$refs.dialogConfirmButton?.focus());
|
|
1029
|
-
});
|
|
1030
|
-
},
|
|
1031
|
-
/**
|
|
1032
|
-
* @param {"about"|"html"} tab
|
|
1033
|
-
*/
|
|
1034
|
-
openAbout(tab) {
|
|
1035
|
-
this.aboutTab = tab;
|
|
1036
|
-
this.isAboutOpen = true;
|
|
1037
|
-
this.$nextTick(() => this.$refs.aboutCloseButton?.focus());
|
|
1038
1668
|
},
|
|
1039
|
-
|
|
1040
|
-
|
|
1669
|
+
watch: {
|
|
1670
|
+
// Used by the CSS to size the text.
|
|
1671
|
+
textScale: {
|
|
1672
|
+
handler(textScale) {
|
|
1673
|
+
document.documentElement.style.setProperty('--text-scale', textScale);
|
|
1674
|
+
},
|
|
1675
|
+
immediate: true,
|
|
1676
|
+
},
|
|
1677
|
+
theme: {
|
|
1678
|
+
handler(theme) {
|
|
1679
|
+
document.documentElement.dataset.theme = theme;
|
|
1680
|
+
},
|
|
1681
|
+
immediate: true,
|
|
1682
|
+
},
|
|
1683
|
+
// Like the browser's console, keep showing the newest output unless the
|
|
1684
|
+
// user has scrolled up to look at older output.
|
|
1685
|
+
'displays.length'() {
|
|
1686
|
+
if (this.isDisplaysScrolledToBottom) {
|
|
1687
|
+
this.$nextTick(() => {
|
|
1688
|
+
const scroller = this.$refs.displaysScroller;
|
|
1689
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1041
1693
|
},
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1694
|
+
mounted() {
|
|
1695
|
+
addEventListener('mousemove', this.onWindowMouseMove);
|
|
1696
|
+
addEventListener('mousedown', this.onWindowMouseDown);
|
|
1697
|
+
addEventListener('keydown', this.onWindowKeyDown);
|
|
1698
|
+
addEventListener('resize', this.onWindowResize);
|
|
1699
|
+
addEventListener('mouseup', this.onWindowMouseUp);
|
|
1700
|
+
addEventListener('error', this.onWindowError);
|
|
1701
|
+
darkSchemeQuery.addEventListener('change', e => this.prefersDark = e.matches);
|
|
1702
|
+
document.addEventListener('fullscreenchange', () => this.isFullscreen = !!document.fullscreenElement);
|
|
1703
|
+
|
|
1704
|
+
// A pop-out window sends its state back to the page when it is closed.
|
|
1705
|
+
if (this.isPopOut) {
|
|
1706
|
+
addEventListener('pagehide', () => this.hasSentState || this.bringBack());
|
|
1046
1707
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1708
|
+
|
|
1709
|
+
// Hidden code that came before all visible code gets run immediately
|
|
1710
|
+
// (unless this is a pop-out window still waiting for code to finish).
|
|
1711
|
+
if (!this.runningCount) this.runHiddenGroups();
|
|
1712
|
+
|
|
1713
|
+
// Shows the loading screen for at least a moment and then fades it out
|
|
1714
|
+
// (but not in a pop-out window which should appear right away).
|
|
1715
|
+
setTimeout(
|
|
1716
|
+
() => document.querySelector('#splash').classList.add('hidden'),
|
|
1717
|
+
Math.max(0, (this.isPopOut ? 0 : MIN_LOADING_TIME) - performance.now())
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
})
|
|
1721
|
+
.component('ace-editor', getAceComponentProps())
|
|
1722
|
+
.component('prism', getPrismComponentProps())
|
|
1723
|
+
.component('icon', getIconComponentProps())
|
|
1724
|
+
.component('js-value', getJSValueComponentProps())
|
|
1725
|
+
.mount(document.querySelector('#vueApp'));
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function getAceComponentProps() {
|
|
1729
|
+
return {
|
|
1730
|
+
data() {
|
|
1731
|
+
return {
|
|
1732
|
+
annotations: [],
|
|
1733
|
+
};
|
|
1734
|
+
},
|
|
1735
|
+
props: [
|
|
1736
|
+
'fontSize',
|
|
1737
|
+
'height',
|
|
1738
|
+
'keybinding',
|
|
1739
|
+
'language',
|
|
1740
|
+
'modelValue',
|
|
1741
|
+
'readOnly',
|
|
1742
|
+
'theme',
|
|
1743
|
+
'width',
|
|
1744
|
+
],
|
|
1745
|
+
computed: {
|
|
1746
|
+
infoNotes() {
|
|
1747
|
+
return this.annotations.filter(({type}) => type === 'info');
|
|
1072
1748
|
},
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
this.dialog = null;
|
|
1076
|
-
dialog?.resolve(isConfirmed);
|
|
1749
|
+
warningNotes() {
|
|
1750
|
+
return this.annotations.filter(({type}) => type === 'warning');
|
|
1077
1751
|
},
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
* automatically following the width of the console.
|
|
1081
|
-
* @param {"horizontal"|"vertical"} orient
|
|
1082
|
-
*/
|
|
1083
|
-
setDividerOrient(orient) {
|
|
1084
|
-
this.isDividerOrientAuto = false;
|
|
1085
|
-
this.dividerOrient = orient;
|
|
1752
|
+
errorNotes() {
|
|
1753
|
+
return this.annotations.filter(({type}) => type === 'error');
|
|
1086
1754
|
},
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
this.
|
|
1090
|
-
|
|
1755
|
+
style() {
|
|
1756
|
+
return {
|
|
1757
|
+
height: this.height != null
|
|
1758
|
+
? 'number' === typeof this.height
|
|
1759
|
+
? this.height + 'px'
|
|
1760
|
+
: this.height
|
|
1761
|
+
: '150px',
|
|
1762
|
+
width: this.width != null
|
|
1763
|
+
? 'number' === typeof this.width
|
|
1764
|
+
? this.width + 'px'
|
|
1765
|
+
: this.width
|
|
1766
|
+
: '100%',
|
|
1767
|
+
};
|
|
1091
1768
|
},
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
this.isDisplaysScrolledToBottom = scrollHeight - scrollTop - clientHeight < 8;
|
|
1769
|
+
modeSig() {
|
|
1770
|
+
return `ace/mode/${this.language ?? 'text'}`;
|
|
1095
1771
|
},
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const y = evt.pageY - rect.top;
|
|
1101
|
-
const w = rect.width;
|
|
1102
|
-
const h = rect.height;
|
|
1103
|
-
const pct = 100 - 100 * Math.min(
|
|
1104
|
-
Math.max(
|
|
1105
|
-
0.2,
|
|
1106
|
-
dividerOrient === 'vertical' ? x / w : (y / h)
|
|
1107
|
-
),
|
|
1108
|
-
0.8
|
|
1772
|
+
themeSig() {
|
|
1773
|
+
let theme = `${this.theme ?? 'light'}`.replace(
|
|
1774
|
+
/^light$|(^dark$)/i,
|
|
1775
|
+
(_, isDark) => `cloud_editor${isDark ? '_dark' : ''}`
|
|
1109
1776
|
);
|
|
1110
|
-
return
|
|
1111
|
-
},
|
|
1112
|
-
onMainElemMouseDown(evt) {
|
|
1113
|
-
if (evt.target === this.$refs.mainDivider) {
|
|
1114
|
-
this.isMovingDivider = true;
|
|
1115
|
-
this.tempDividerPct = this.getEditorPct(evt);
|
|
1116
|
-
}
|
|
1117
|
-
},
|
|
1118
|
-
onWindowMouseMove(evt) {
|
|
1119
|
-
if (this.isMovingDivider) {
|
|
1120
|
-
this.tempDividerPct = this.getEditorPct(evt);
|
|
1121
|
-
}
|
|
1122
|
-
},
|
|
1123
|
-
onWindowMouseUp(evt) {
|
|
1124
|
-
if (this.isMovingDivider) {
|
|
1125
|
-
this.isMovingDivider = false;
|
|
1126
|
-
this.dividerPct = this.getEditorPct(evt);
|
|
1127
|
-
}
|
|
1128
|
-
},
|
|
1129
|
-
onWindowError(evt) {
|
|
1130
|
-
this.displays.push({
|
|
1131
|
-
type: 'error',
|
|
1132
|
-
message: evt.error?.stack ?? evt.message ?? evt.error?.message ?? `${evt.error}`,
|
|
1133
|
-
line: evt.lineno,
|
|
1134
|
-
column: evt.colno,
|
|
1135
|
-
})
|
|
1777
|
+
return `ace/theme/${theme}`;
|
|
1136
1778
|
}
|
|
1137
1779
|
},
|
|
1138
1780
|
watch: {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
document.documentElement.dataset.theme = theme;
|
|
1142
|
-
},
|
|
1143
|
-
immediate: true,
|
|
1781
|
+
themeSig(newValue) {
|
|
1782
|
+
this.editor.setTheme(newValue);
|
|
1144
1783
|
},
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
'displays.length'() {
|
|
1148
|
-
if (this.isDisplaysScrolledToBottom) {
|
|
1149
|
-
this.$nextTick(() => {
|
|
1150
|
-
const scroller = this.$refs.displaysScroller;
|
|
1151
|
-
scroller.scrollTop = scroller.scrollHeight;
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1784
|
+
fontSize(newValue) {
|
|
1785
|
+
if (newValue) this.editor.setFontSize(newValue);
|
|
1154
1786
|
},
|
|
1787
|
+
modelValue(newValue) {
|
|
1788
|
+
if (newValue !== this.editor.getValue()) {
|
|
1789
|
+
// -1 puts the cursor at the start instead of selecting everything.
|
|
1790
|
+
this.editor.setValue(newValue, -1);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1155
1793
|
},
|
|
1156
|
-
mounted() {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1794
|
+
async mounted() {
|
|
1795
|
+
// Get the ace editor set up with the language and theme.
|
|
1796
|
+
const editor = ace.edit(this.$refs.editor, {
|
|
1797
|
+
value: this.modelValue,
|
|
1798
|
+
mode: this.modeSig,
|
|
1799
|
+
theme: this.themeSig,
|
|
1800
|
+
});
|
|
1801
|
+
this.editor = editor;
|
|
1802
|
+
|
|
1803
|
+
// Read-only editors don't need syntax checking (eg. hints in the gutter).
|
|
1804
|
+
if (this.fontSize) editor.setFontSize(this.fontSize);
|
|
1805
|
+
|
|
1806
|
+
if (this.readOnly) {
|
|
1807
|
+
editor.setReadOnly(true);
|
|
1808
|
+
editor.setHighlightActiveLine(false);
|
|
1809
|
+
editor.session.setUseWorker(false);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Use VSCode keybinding but remove the CTRL+ENTER and CTRL+SHIFT+ENTER
|
|
1813
|
+
// (or on Mac CMD+ENTER and CMD+SHIFT+ENTER) commands so that they can be
|
|
1814
|
+
// caught by custom code.
|
|
1815
|
+
editor.setKeyboardHandler('ace/keyboard/vscode');
|
|
1816
|
+
editor.commands.removeCommand('addLineAfter');
|
|
1817
|
+
editor.commands.removeCommand('addLineBefore');
|
|
1818
|
+
|
|
1819
|
+
// When the value in the ace editor is updated make sure to emit the
|
|
1820
|
+
// modelValue update event.
|
|
1821
|
+
editor.on('change', () => this.$emit('update:modelValue', editor.getValue()));
|
|
1822
|
+
|
|
1823
|
+
// When the annotations change in the editor go ahead and trigger the
|
|
1824
|
+
// different note events if necessary.
|
|
1825
|
+
editor.session.on("changeAnnotation", () => {
|
|
1826
|
+
const oldErrorsJSON = JSON.stringify(this.errorNotes);
|
|
1827
|
+
const oldInfosJSON = JSON.stringify(this.infoNotes);
|
|
1828
|
+
const oldWarningsJSON = JSON.stringify(this.warningNotes);
|
|
1829
|
+
this.annotations = editor.getSession().getAnnotations();
|
|
1830
|
+
const {errorNotes, infoNotes, warningNotes} = this;
|
|
1831
|
+
if (oldErrorsJSON !== JSON.stringify(errorNotes)) {
|
|
1832
|
+
this.$emit('changeErrorNotes', errorNotes);
|
|
1833
|
+
}
|
|
1834
|
+
if (oldInfosJSON !== JSON.stringify(infoNotes)) {
|
|
1835
|
+
this.$emit('changeInfoNotes', infoNotes);
|
|
1836
|
+
}
|
|
1837
|
+
if (oldWarningsJSON !== JSON.stringify(warningNotes)) {
|
|
1838
|
+
this.$emit('changeWarningNotes', warningNotes);
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
// Attempt to capture key combos.
|
|
1843
|
+
editor.textInput.getElement().addEventListener('keydown', (evt) => {
|
|
1844
|
+
const isComboKeyCode = evt.keyCode === 16 // SHIFT
|
|
1845
|
+
|| evt.keyCode === 17 // CTRL
|
|
1846
|
+
|| evt.keyCode === 18 // ALT or OPT
|
|
1847
|
+
|| evt.keyCode === 91 // META
|
|
1848
|
+
|| evt.keyCode === 92 // META
|
|
1849
|
+
|| evt.keyCode === 93 // META;
|
|
1850
|
+
|
|
1851
|
+
// Dont do anything if only a combo key is being pressed.
|
|
1852
|
+
if (isComboKeyCode) return;
|
|
1853
|
+
|
|
1854
|
+
// If this is really a key combo then go ahead and emit that event.
|
|
1855
|
+
const isCombo = evt.metaKey
|
|
1856
|
+
|| evt.ctrlKey
|
|
1857
|
+
|| evt.altKey
|
|
1858
|
+
|| (evt.shiftKey && (evt.key ?? '').length > 1);
|
|
1859
|
+
if (isCombo) this.$emit('keyCombo', evt);
|
|
1860
|
+
});
|
|
1205
1861
|
},
|
|
1206
|
-
style
|
|
1862
|
+
template: '<div ref="editor" :style="style"></div>',
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function getPrismComponentProps() {
|
|
1867
|
+
return {
|
|
1868
|
+
data() {
|
|
1207
1869
|
return {
|
|
1208
|
-
|
|
1209
|
-
? 'number' === typeof this.height
|
|
1210
|
-
? this.height + 'px'
|
|
1211
|
-
: this.height
|
|
1212
|
-
: '150px',
|
|
1213
|
-
width: this.width != null
|
|
1214
|
-
? 'number' === typeof this.width
|
|
1215
|
-
? this.width + 'px'
|
|
1216
|
-
: this.width
|
|
1217
|
-
: '100%',
|
|
1870
|
+
annotations: [],
|
|
1218
1871
|
};
|
|
1219
1872
|
},
|
|
1220
|
-
|
|
1221
|
-
|
|
1873
|
+
props: ['code', 'isDark', 'language', 'lineNumbers', 'matchBraces'],
|
|
1874
|
+
computed: {
|
|
1875
|
+
html() {},
|
|
1876
|
+
preStyle() {
|
|
1877
|
+
return {
|
|
1878
|
+
filter: (this.isDark ?? false) !== false
|
|
1879
|
+
? 'none'
|
|
1880
|
+
: 'invert(1) hue-rotate(180deg) brightness(1.1)',
|
|
1881
|
+
margin: 0,
|
|
1882
|
+
paddingTop: '0.5em',
|
|
1883
|
+
paddingBottom: '0.5em'
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1222
1886
|
},
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
);
|
|
1228
|
-
return `ace/theme/${theme}`;
|
|
1229
|
-
}
|
|
1230
|
-
},
|
|
1231
|
-
watch: {
|
|
1232
|
-
themeSig(newValue) {
|
|
1233
|
-
this.editor.setTheme(newValue);
|
|
1887
|
+
watch: {
|
|
1888
|
+
code() { this.redraw(); },
|
|
1889
|
+
language() { this.redraw(); },
|
|
1890
|
+
lineNumbers() { this.redraw(); },
|
|
1891
|
+
matchBraces() { this.redraw(); },
|
|
1234
1892
|
},
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
this.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
if (this.readOnly) {
|
|
1253
|
-
editor.setReadOnly(true);
|
|
1254
|
-
editor.setHighlightActiveLine(false);
|
|
1255
|
-
editor.session.setUseWorker(false);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// Use VSCode keybinding but remove the CTRL+ENTER and CTRL+SHIFT+ENTER
|
|
1259
|
-
// (or on Mac CMD+ENTER and CMD+SHIFT+ENTER) commands so that they can be
|
|
1260
|
-
// caught by custom code.
|
|
1261
|
-
editor.setKeyboardHandler('ace/keyboard/vscode');
|
|
1262
|
-
editor.commands.removeCommand('addLineAfter');
|
|
1263
|
-
editor.commands.removeCommand('addLineBefore');
|
|
1264
|
-
|
|
1265
|
-
// When the value in the ace editor is updated make sure to emit the
|
|
1266
|
-
// modelValue update event.
|
|
1267
|
-
editor.on('change', () => this.$emit('update:modelValue', editor.getValue()));
|
|
1268
|
-
|
|
1269
|
-
// When the annotations change in the editor go ahead and trigger the
|
|
1270
|
-
// different note events if necessary.
|
|
1271
|
-
editor.session.on("changeAnnotation", () => {
|
|
1272
|
-
const oldErrorsJSON = JSON.stringify(this.errorNotes);
|
|
1273
|
-
const oldInfosJSON = JSON.stringify(this.infoNotes);
|
|
1274
|
-
const oldWarningsJSON = JSON.stringify(this.warningNotes);
|
|
1275
|
-
this.annotations = editor.getSession().getAnnotations();
|
|
1276
|
-
const {errorNotes, infoNotes, warningNotes} = this;
|
|
1277
|
-
if (oldErrorsJSON !== JSON.stringify(errorNotes)) {
|
|
1278
|
-
this.$emit('changeErrorNotes', errorNotes);
|
|
1279
|
-
}
|
|
1280
|
-
if (oldInfosJSON !== JSON.stringify(infoNotes)) {
|
|
1281
|
-
this.$emit('changeInfoNotes', infoNotes);
|
|
1282
|
-
}
|
|
1283
|
-
if (oldWarningsJSON !== JSON.stringify(warningNotes)) {
|
|
1284
|
-
this.$emit('changeWarningNotes', warningNotes);
|
|
1285
|
-
}
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
|
-
// Attempt to capture key combos.
|
|
1289
|
-
editor.textInput.getElement().addEventListener('keydown', (evt) => {
|
|
1290
|
-
const isComboKeyCode = evt.keyCode === 16 // SHIFT
|
|
1291
|
-
|| evt.keyCode === 17 // CTRL
|
|
1292
|
-
|| evt.keyCode === 18 // ALT or OPT
|
|
1293
|
-
|| evt.keyCode === 91 // META
|
|
1294
|
-
|| evt.keyCode === 92 // META
|
|
1295
|
-
|| evt.keyCode === 93 // META;
|
|
1296
|
-
|
|
1297
|
-
// Dont do anything if only a combo key is being pressed.
|
|
1298
|
-
if (isComboKeyCode) return;
|
|
1299
|
-
|
|
1300
|
-
// If this is really a key combo then go ahead and emit that event.
|
|
1301
|
-
const isCombo = evt.metaKey
|
|
1302
|
-
|| evt.ctrlKey
|
|
1303
|
-
|| evt.altKey
|
|
1304
|
-
|| (evt.shiftKey && (evt.key ?? '').length > 1);
|
|
1305
|
-
if (isCombo) this.$emit('keyCombo', evt);
|
|
1306
|
-
});
|
|
1307
|
-
},
|
|
1308
|
-
template: '<div ref="editor" :style="style"></div>',
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
function getPrismComponentProps() {
|
|
1313
|
-
return {
|
|
1314
|
-
data() {
|
|
1315
|
-
return {
|
|
1316
|
-
annotations: [],
|
|
1317
|
-
};
|
|
1318
|
-
},
|
|
1319
|
-
props: ['code', 'isDark', 'language', 'lineNumbers', 'matchBraces'],
|
|
1320
|
-
computed: {
|
|
1321
|
-
html() {},
|
|
1322
|
-
preStyle() {
|
|
1323
|
-
return {
|
|
1324
|
-
filter: (this.isDark ?? false) !== false
|
|
1325
|
-
? 'none'
|
|
1326
|
-
: 'invert(1) hue-rotate(180deg) brightness(1.1)',
|
|
1327
|
-
margin: 0,
|
|
1328
|
-
paddingTop: '0.5em',
|
|
1329
|
-
paddingBottom: '0.5em'
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
},
|
|
1333
|
-
watch: {
|
|
1334
|
-
code() { this.redraw(); },
|
|
1335
|
-
language() { this.redraw(); },
|
|
1336
|
-
lineNumbers() { this.redraw(); },
|
|
1337
|
-
matchBraces() { this.redraw(); },
|
|
1338
|
-
},
|
|
1339
|
-
methods: {
|
|
1340
|
-
async redraw() {
|
|
1341
|
-
/** @type {HTMLPreElement} */
|
|
1342
|
-
const preElem = this.$refs.pre;
|
|
1343
|
-
/** @type {HTMLElement} */
|
|
1344
|
-
const codeElem = this.$refs.code;
|
|
1345
|
-
|
|
1346
|
-
codeElem.textContent = this.code;
|
|
1347
|
-
|
|
1348
|
-
codeElem.className = `language-javascript`;
|
|
1349
|
-
preElem.className = '';
|
|
1350
|
-
|
|
1351
|
-
for (const propName of ['lineNumbers', 'matchBraces']) {
|
|
1352
|
-
if (this[propName] != null) {
|
|
1353
|
-
const dashedPropName = propName.replace(/[A-Z]+/g, '-$&').toLowerCase();
|
|
1354
|
-
preElem.className += ' ' + dashedPropName;
|
|
1893
|
+
methods: {
|
|
1894
|
+
async redraw() {
|
|
1895
|
+
/** @type {HTMLPreElement} */
|
|
1896
|
+
const preElem = this.$refs.pre;
|
|
1897
|
+
/** @type {HTMLElement} */
|
|
1898
|
+
const codeElem = this.$refs.code;
|
|
1899
|
+
|
|
1900
|
+
codeElem.textContent = this.code;
|
|
1901
|
+
|
|
1902
|
+
codeElem.className = `language-javascript`;
|
|
1903
|
+
preElem.className = '';
|
|
1904
|
+
|
|
1905
|
+
for (const propName of ['lineNumbers', 'matchBraces']) {
|
|
1906
|
+
if (this[propName] != null) {
|
|
1907
|
+
const dashedPropName = propName.replace(/[A-Z]+/g, '-$&').toLowerCase();
|
|
1908
|
+
preElem.className += ' ' + dashedPropName;
|
|
1909
|
+
}
|
|
1355
1910
|
}
|
|
1911
|
+
|
|
1912
|
+
// Wait for all scripts to load before trying to syntax highlight the
|
|
1913
|
+
// code element.
|
|
1914
|
+
Prism.highlightElement(codeElem);
|
|
1356
1915
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
async mounted() {
|
|
1364
|
-
await this.redraw();
|
|
1365
|
-
},
|
|
1366
|
-
template: '<pre ref="pre" :style="preStyle"><code ref="code"></code></pre>',
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function getIconComponentProps() {
|
|
1371
|
-
return {
|
|
1372
|
-
props: ['name'],
|
|
1373
|
-
computed: {
|
|
1374
|
-
svgCode() {
|
|
1375
|
-
const codes = {
|
|
1376
|
-
// https://icon-sets.iconify.design/carbon/unknown/
|
|
1377
|
-
missing: '<svg viewBox="0 0 32 32"><circle cx="16" cy="22.5" r="1.5" fill="currentColor"/><path fill="currentColor" d="M17 19h-2v-4h2c1.103 0 2-.897 2-2s-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.5h-2V13c0-2.206 1.794-4 4-4h2c2.206 0 4 1.794 4 4s-1.794 4-4 4z"/><path fill="currentColor" d="M29.391 14.527L17.473 2.609A2.078 2.078 0 0 0 16 2c-.533 0-1.067.203-1.473.609L2.609 14.527C2.203 14.933 2 15.466 2 16s.203 1.067.609 1.473L14.526 29.39c.407.407.941.61 1.474.61s1.067-.203 1.473-.609L29.39 17.474c.407-.407.61-.94.61-1.474s-.203-1.067-.609-1.473M16 28.036L3.965 16L16 3.964L28.036 16z"/></svg>',
|
|
1378
|
-
// https://icon-sets.iconify.design/radix-icons/trash/
|
|
1379
|
-
trash: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M5.5 1a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1zM3 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H11v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4h-.5a.5.5 0 0 1-.5-.5M5 4h5v8H5z" clip-rule="evenodd"/></svg>',
|
|
1380
|
-
// https://icon-sets.iconify.design/radix-icons/play/
|
|
1381
|
-
play: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M3.242 2.322a.5.5 0 0 1 .491-.014l9 4.75a.5.5 0 0 1 0 .884l-9 4.75A.5.5 0 0 1 3 12.25v-9.5a.5.5 0 0 1 .242-.428M4 3.579v7.842L11.429 7.5z" clip-rule="evenodd"/></svg>',
|
|
1382
|
-
// https://icon-sets.iconify.design/radix-icons/update/
|
|
1383
|
-
refresh: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.903 7.297c0 3.044 2.207 5.118 4.686 5.547a.521.521 0 1 1-.178 1.027C3.5 13.367.861 10.913.861 7.297c0-1.537.699-2.745 1.515-3.663c.585-.658 1.254-1.193 1.792-1.602H2.532a.5.5 0 0 1 0-1h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V2.686l-.001.002c-.572.43-1.27.957-1.875 1.638c-.715.804-1.253 1.776-1.253 2.97m11.108.406c0-3.012-2.16-5.073-4.607-5.533a.521.521 0 1 1 .192-1.024c2.874.54 5.457 2.98 5.457 6.557c0 1.537-.699 2.744-1.515 3.663c-.585.658-1.254 1.193-1.792 1.602h1.636a.5.5 0 1 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v1.845h.002c.571-.432 1.27-.958 1.874-1.64c.715-.803 1.253-1.775 1.253-2.97" clip-rule="evenodd"/></svg>',
|
|
1384
|
-
// https://icon-sets.iconify.design/radix-icons/view-horizontal/
|
|
1385
|
-
horizontalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.5 2h12a.5.5 0 0 1 .5.5V7H1V2.5a.5.5 0 0 1 .5-.5M1 8v4.5a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V8zM0 2.5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
|
|
1386
|
-
// https://icon-sets.iconify.design/radix-icons/view-vertical/
|
|
1387
|
-
verticalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M8 2h5.5a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-.5.5H8zM7 2H1.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5H7zm-7 .5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
|
|
1388
|
-
// https://icon-sets.iconify.design/vaadin/play/
|
|
1389
|
-
play: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M2 1v14l12-7z"/></svg>',
|
|
1390
|
-
// https://icon-sets.iconify.design/mdi/error-outline/
|
|
1391
|
-
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>',
|
|
1392
|
-
// Icons similar to those in the browser's console.
|
|
1393
|
-
close: '<svg viewBox="0 0 16 16"><path d="M4 4l8 8m0-8l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',
|
|
1394
|
-
clear: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.75 12.25l8.5-8.5" stroke="currentColor" stroke-width="1.5"/></svg>',
|
|
1395
|
-
copyToEditor: '<svg viewBox="0 0 16 16"><path d="M6 3.5L2.5 7 6 10.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7h6.5a4 4 0 0 1 4 4v1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
|
1396
|
-
spinner: '<svg viewBox="0 0 16 16" class="spin"><path d="M8 1.75a6.25 6.25 0 1 1-6.25 6.25" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>',
|
|
1397
|
-
chevron: '<svg viewBox="0 0 16 16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1398
|
-
consoleError: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="currentColor"/><path d="M5.5 5.5l5 5m0-5l-5 5" stroke="#fff" stroke-width="1.75" stroke-linecap="round"/></svg>',
|
|
1399
|
-
consoleWarning: '<svg viewBox="0 0 16 16"><path d="M8 1.75L14.75 14H1.25z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 6v3.5" stroke="#202124" stroke-width="1.75" stroke-linecap="round"/><circle cx="8" cy="11.75" r="1" fill="#202124"/></svg>',
|
|
1400
|
-
};
|
|
1401
|
-
return (codes[this.name] ?? codes.missing).replace('<svg', '$& xmlns="http://www.w3.org/2000/svg" style="height: 1em; width: 1em; display: inline-block; transform: translateY(0.1em);"');
|
|
1402
|
-
}
|
|
1403
|
-
},
|
|
1404
|
-
template: '<span v-html="svgCode"></span>'
|
|
1916
|
+
},
|
|
1917
|
+
async mounted() {
|
|
1918
|
+
await this.redraw();
|
|
1919
|
+
},
|
|
1920
|
+
template: '<pre ref="pre" :style="preStyle"><code ref="code"></code></pre>',
|
|
1921
|
+
};
|
|
1405
1922
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1923
|
+
|
|
1924
|
+
function getIconComponentProps() {
|
|
1925
|
+
return {
|
|
1926
|
+
props: ['name'],
|
|
1927
|
+
computed: {
|
|
1928
|
+
svgCode() {
|
|
1929
|
+
const codes = {
|
|
1930
|
+
// https://icon-sets.iconify.design/carbon/unknown/
|
|
1931
|
+
missing: '<svg viewBox="0 0 32 32"><circle cx="16" cy="22.5" r="1.5" fill="currentColor"/><path fill="currentColor" d="M17 19h-2v-4h2c1.103 0 2-.897 2-2s-.897-2-2-2h-2c-1.103 0-2 .897-2 2v.5h-2V13c0-2.206 1.794-4 4-4h2c2.206 0 4 1.794 4 4s-1.794 4-4 4z"/><path fill="currentColor" d="M29.391 14.527L17.473 2.609A2.078 2.078 0 0 0 16 2c-.533 0-1.067.203-1.473.609L2.609 14.527C2.203 14.933 2 15.466 2 16s.203 1.067.609 1.473L14.526 29.39c.407.407.941.61 1.474.61s1.067-.203 1.473-.609L29.39 17.474c.407-.407.61-.94.61-1.474s-.203-1.067-.609-1.473M16 28.036L3.965 16L16 3.964L28.036 16z"/></svg>',
|
|
1932
|
+
// https://icon-sets.iconify.design/radix-icons/trash/
|
|
1933
|
+
trash: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M5.5 1a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1zM3 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H11v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4h-.5a.5.5 0 0 1-.5-.5M5 4h5v8H5z" clip-rule="evenodd"/></svg>',
|
|
1934
|
+
// https://icon-sets.iconify.design/radix-icons/play/
|
|
1935
|
+
play: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M3.242 2.322a.5.5 0 0 1 .491-.014l9 4.75a.5.5 0 0 1 0 .884l-9 4.75A.5.5 0 0 1 3 12.25v-9.5a.5.5 0 0 1 .242-.428M4 3.579v7.842L11.429 7.5z" clip-rule="evenodd"/></svg>',
|
|
1936
|
+
// https://icon-sets.iconify.design/radix-icons/update/
|
|
1937
|
+
refresh: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.903 7.297c0 3.044 2.207 5.118 4.686 5.547a.521.521 0 1 1-.178 1.027C3.5 13.367.861 10.913.861 7.297c0-1.537.699-2.745 1.515-3.663c.585-.658 1.254-1.193 1.792-1.602H2.532a.5.5 0 0 1 0-1h3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0V2.686l-.001.002c-.572.43-1.27.957-1.875 1.638c-.715.804-1.253 1.776-1.253 2.97m11.108.406c0-3.012-2.16-5.073-4.607-5.533a.521.521 0 1 1 .192-1.024c2.874.54 5.457 2.98 5.457 6.557c0 1.537-.699 2.744-1.515 3.663c-.585.658-1.254 1.193-1.792 1.602h1.636a.5.5 0 1 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v1.845h.002c.571-.432 1.27-.958 1.874-1.64c.715-.803 1.253-1.775 1.253-2.97" clip-rule="evenodd"/></svg>',
|
|
1938
|
+
// https://icon-sets.iconify.design/radix-icons/view-horizontal/
|
|
1939
|
+
horizontalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M1.5 2h12a.5.5 0 0 1 .5.5V7H1V2.5a.5.5 0 0 1 .5-.5M1 8v4.5a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V8zM0 2.5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
|
|
1940
|
+
// https://icon-sets.iconify.design/radix-icons/view-vertical/
|
|
1941
|
+
verticalView: '<svg viewBox="0 0 15 15"><path fill="currentColor" fill-rule="evenodd" d="M8 2h5.5a.5.5 0 0 1 .5.5v10a.5.5 0 0 1-.5.5H8zM7 2H1.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5H7zm-7 .5A1.5 1.5 0 0 1 1.5 1h12A1.5 1.5 0 0 1 15 2.5v10a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 0 12.5z" clip-rule="evenodd"/></svg>',
|
|
1942
|
+
// https://icon-sets.iconify.design/vaadin/play/
|
|
1943
|
+
play: '<svg viewBox="0 0 16 16"><path fill="currentColor" d="M2 1v14l12-7z"/></svg>',
|
|
1944
|
+
// https://icon-sets.iconify.design/mdi/error-outline/
|
|
1945
|
+
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>',
|
|
1946
|
+
// Icons similar to those in the browser's console.
|
|
1947
|
+
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>',
|
|
1948
|
+
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>',
|
|
1949
|
+
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>',
|
|
1950
|
+
check: '<svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1951
|
+
popOut: '<svg viewBox="0 0 16 16"><path d="M9.5 2.5h4v4M13.5 2.5L8 8M12 9.5v3a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1952
|
+
popIn: '<svg viewBox="0 0 16 16"><path d="M12 8.5H8V4.5M8 8.5l5.5-5.5M12 10.5v2a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1953
|
+
code: '<svg viewBox="0 0 16 16"><path d="M5.5 4.5L2 8l3.5 3.5M10.5 4.5L14 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1954
|
+
info: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.25" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 7.25v4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="8" cy="5" r="1" fill="currentColor"/></svg>',
|
|
1955
|
+
result: '<svg viewBox="0 0 16 16"><path d="M9 4.5L5.5 8 9 11.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="8" r="1.1" fill="currentColor"/></svg>',
|
|
1956
|
+
close: '<svg viewBox="0 0 16 16"><path d="M4 4l8 8m0-8l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',
|
|
1957
|
+
clear: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.75 12.25l8.5-8.5" stroke="currentColor" stroke-width="1.5"/></svg>',
|
|
1958
|
+
copyToEditor: '<svg viewBox="0 0 16 16"><path d="M6 3.5L2.5 7 6 10.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7h6.5a4 4 0 0 1 4 4v1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
|
1959
|
+
spinner: '<svg viewBox="0 0 16 16" class="spin"><path d="M8 1.75a6.25 6.25 0 1 1-6.25 6.25" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>',
|
|
1960
|
+
chevron: '<svg viewBox="0 0 16 16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1961
|
+
consoleError: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="currentColor"/><path d="M5.5 5.5l5 5m0-5l-5 5" stroke="#fff" stroke-width="1.75" stroke-linecap="round"/></svg>',
|
|
1962
|
+
consoleWarning: '<svg viewBox="0 0 16 16"><path d="M8 1.75L14.75 14H1.25z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 6v3.5" stroke="#202124" stroke-width="1.75" stroke-linecap="round"/><circle cx="8" cy="11.75" r="1" fill="#202124"/></svg>',
|
|
1963
|
+
};
|
|
1964
|
+
return (codes[this.name] ?? codes.missing).replace('<svg', '$& xmlns="http://www.w3.org/2000/svg" style="height: 1em; width: 1em; display: inline-block; transform: translateY(0.1em);"');
|
|
1421
1965
|
}
|
|
1422
|
-
}
|
|
1423
|
-
},
|
|
1424
|
-
computed: {
|
|
1425
|
-
isPartialDescription() {
|
|
1426
|
-
return this.description.entries === undefined;
|
|
1427
1966
|
},
|
|
1428
|
-
|
|
1429
|
-
|
|
1967
|
+
template: '<span v-html="svgCode"></span>'
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function getJSValueComponentProps() {
|
|
1972
|
+
return {
|
|
1973
|
+
props: ['description', 'path', 'name', 'isDimName'],
|
|
1974
|
+
data() {
|
|
1975
|
+
return {
|
|
1976
|
+
isExpanded: false,
|
|
1977
|
+
hasBeenExpanded: false,
|
|
1978
|
+
};
|
|
1430
1979
|
},
|
|
1431
|
-
|
|
1432
|
-
|
|
1980
|
+
watch: {
|
|
1981
|
+
isExpanded(newValue) {
|
|
1982
|
+
if (newValue && !this.hasBeenExpanded) {
|
|
1983
|
+
this.hasBeenExpanded = newValue;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1433
1986
|
},
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
this.
|
|
1440
|
-
|
|
1987
|
+
computed: {
|
|
1988
|
+
isPartialDescription() {
|
|
1989
|
+
return this.description.entries === undefined;
|
|
1990
|
+
},
|
|
1991
|
+
isExpandable() {
|
|
1992
|
+
return !this.description.isPrimitive;
|
|
1993
|
+
},
|
|
1994
|
+
isEntry() {
|
|
1995
|
+
return this.name != null;
|
|
1996
|
+
},
|
|
1997
|
+
classNames() {
|
|
1998
|
+
return [
|
|
1999
|
+
'js-value',
|
|
2000
|
+
// Entries are always on their own line just like expanded values.
|
|
2001
|
+
(this.isEntry || (this.isExpandable && this.isExpanded)) ? 'd-block' : 'd-inline-block',
|
|
2002
|
+
this.isExpanded ? 'expanded' : '',
|
|
2003
|
+
];
|
|
2004
|
+
},
|
|
2005
|
+
entryGroups() {
|
|
2006
|
+
// protoEntries only ever contains the [[Prototype]] entry.
|
|
2007
|
+
return [
|
|
2008
|
+
{ key: 'entries', isDim: entry => entry[2] === false },
|
|
2009
|
+
{ key: 'protoEntries', isDim: () => true },
|
|
2010
|
+
];
|
|
2011
|
+
}
|
|
1441
2012
|
},
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
},
|
|
1450
|
-
methods: {
|
|
1451
|
-
toggleExpanded() {
|
|
1452
|
-
if (!this.isExpandable) return;
|
|
1453
|
-
this.isExpanded = !this.isExpanded;
|
|
1454
|
-
if (this.isExpanded && this.isPartialDescription) {
|
|
1455
|
-
messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
|
|
2013
|
+
methods: {
|
|
2014
|
+
toggleExpanded() {
|
|
2015
|
+
if (!this.isExpandable) return;
|
|
2016
|
+
this.isExpanded = !this.isExpanded;
|
|
2017
|
+
if (this.isExpanded && this.isPartialDescription) {
|
|
2018
|
+
messageParent({target: 'runner', func: 'sendDescriptionFor', args: [this.path]});
|
|
2019
|
+
}
|
|
1456
2020
|
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
v-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
</
|
|
2021
|
+
},
|
|
2022
|
+
template: `
|
|
2023
|
+
<div :class="classNames">
|
|
2024
|
+
<div :class="['js-value-header', isExpandable ? 'expandable' : '']" @click="toggleExpanded"><span
|
|
2025
|
+
v-if="isExpandable || isEntry" :class="['arrow', isExpandable ? 'expandable' : '', isExpanded ? 'expanded' : '']"></span><template
|
|
2026
|
+
v-if="isEntry"><span :class="['entry-key', isDimName ? 'dim' : '']">{{ name }}</span>: </template><span
|
|
2027
|
+
v-for="part in description.parts" :class="'t-' + part[0]">{{ part[1] }}</span></div>
|
|
2028
|
+
<div v-if="hasBeenExpanded" v-show="isExpanded" class="expansion">
|
|
2029
|
+
<div v-if="isPartialDescription" class="loading">Loading…</div>
|
|
2030
|
+
<template v-else>
|
|
2031
|
+
<template v-for="group in entryGroups">
|
|
2032
|
+
<js-value
|
|
2033
|
+
v-for="(entry, entryIndex) in description[group.key]"
|
|
2034
|
+
:name="entry[0]"
|
|
2035
|
+
:is-dim-name="group.isDim(entry)"
|
|
2036
|
+
:description="entry[1]"
|
|
2037
|
+
:path="path.concat([group.key, entryIndex])">
|
|
2038
|
+
</js-value>
|
|
2039
|
+
</template>
|
|
1476
2040
|
</template>
|
|
1477
|
-
</
|
|
2041
|
+
</div>
|
|
1478
2042
|
</div>
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2043
|
+
`
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/**
|
|
2048
|
+
* @param {string} jsCode
|
|
2049
|
+
* @returns {{headerLines: string, lines: string, allLines: string}[]}
|
|
2050
|
+
*/
|
|
2051
|
+
function parseJSCodeGroups(jsCode) {
|
|
2052
|
+
return jsCode.split(/\r\n|\r|\n/).reduce(
|
|
2053
|
+
(groups, line) => {
|
|
2054
|
+
const lastGroup = groups[groups.length - 1];
|
|
2055
|
+
const isHeaderLine = /^\/\/[^]*\\\\$/.test(line);
|
|
2056
|
+
const headerLine = isHeaderLine ? line.replace(/^\/+\s*|\s*\\+$/g, '') : line;
|
|
2057
|
+
if (!groups.length || (isHeaderLine && !lastGroup.canAppendHeader)) {
|
|
2058
|
+
groups.push({
|
|
2059
|
+
headerLines: isHeaderLine ? [headerLine] : [],
|
|
2060
|
+
lines: isHeaderLine ? [] : [line],
|
|
2061
|
+
allLines: [line],
|
|
2062
|
+
canAppendHeader: isHeaderLine,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
else if (isHeaderLine) {
|
|
2066
|
+
lastGroup.headerLines.push(headerLine);
|
|
2067
|
+
lastGroup.allLines.push(line);
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
lastGroup.canAppendHeader = false;
|
|
2071
|
+
lastGroup.lines.push(line);
|
|
2072
|
+
lastGroup.allLines.push(line);
|
|
2073
|
+
}
|
|
2074
|
+
return groups;
|
|
2075
|
+
},
|
|
2076
|
+
[]
|
|
2077
|
+
).reduce(
|
|
2078
|
+
(groups, group) => {
|
|
2079
|
+
if (!group.canAppendHeader) {
|
|
2080
|
+
delete group.canAppendHeader;
|
|
2081
|
+
group.headerLines = group.headerLines.join('\n').trimEnd();
|
|
2082
|
+
group.lines = group.lines.join('\n');
|
|
2083
|
+
group.allLines = group.allLines.join('\n');
|
|
2084
|
+
groups.push(group);
|
|
2085
|
+
}
|
|
2086
|
+
return groups;
|
|
2087
|
+
},
|
|
2088
|
+
[]
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Determines the header that should be displayed for a group of code and
|
|
2094
|
+
* whether or not the group's code should be hidden. A group is hidden if its
|
|
2095
|
+
* header starts with `hidePrefix`, in which case anything after the prefix (and
|
|
2096
|
+
* an optional colon) is used as the header.
|
|
2097
|
+
* @param {string} headerLines
|
|
2098
|
+
* @param {string} hidePrefix
|
|
2099
|
+
* @returns {{header: string, isHidden: boolean}}
|
|
2100
|
+
*/
|
|
2101
|
+
function parseGroupHeader(headerLines, hidePrefix) {
|
|
2102
|
+
const isHidden = !!hidePrefix && headerLines.startsWith(hidePrefix);
|
|
2103
|
+
return {
|
|
2104
|
+
header: isHidden
|
|
2105
|
+
? headerLines.slice(hidePrefix.length).replace(/^\s*:?\s*/, '')
|
|
2106
|
+
: headerLines,
|
|
2107
|
+
isHidden,
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* Separates the hidden groups of code from the visible code.
|
|
2113
|
+
* @param {string} jsCode
|
|
2114
|
+
* @param {string} hidePrefix
|
|
2115
|
+
* @returns {{
|
|
2116
|
+
* visibleCode: string,
|
|
2117
|
+
* hiddenGroups: (ReturnType<parseJSCodeGroups>[number] & {runCount: number})[]
|
|
2118
|
+
* }}
|
|
2119
|
+
* `visibleCode` is the code to show in the editor. Each hidden group has a
|
|
2120
|
+
* `runCount` indicating how many visible groups must be run before it runs.
|
|
2121
|
+
*/
|
|
2122
|
+
function extractHiddenGroups(jsCode, hidePrefix) {
|
|
2123
|
+
const visibleGroups = [];
|
|
2124
|
+
const hiddenGroups = [];
|
|
2125
|
+
for (const group of parseJSCodeGroups(jsCode)) {
|
|
2126
|
+
if (parseGroupHeader(group.headerLines, hidePrefix).isHidden) {
|
|
2127
|
+
hiddenGroups.push({...group, runCount: visibleGroups.length});
|
|
1505
2128
|
}
|
|
1506
2129
|
else {
|
|
1507
|
-
|
|
1508
|
-
lastGroup.lines.push(line);
|
|
1509
|
-
lastGroup.allLines.push(line);
|
|
2130
|
+
visibleGroups.push(group);
|
|
1510
2131
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2132
|
+
}
|
|
2133
|
+
return {
|
|
2134
|
+
visibleCode: visibleGroups.map(g => g.allLines).join('\n'),
|
|
2135
|
+
hiddenGroups,
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Finds the last statement in the code if it is an expression so that its
|
|
2141
|
+
* value can be shown.
|
|
2142
|
+
* @param {string} code
|
|
2143
|
+
* @param {"classic"|"module"} blockType
|
|
2144
|
+
* @returns {[number, number]|null}
|
|
2145
|
+
* The start and end index of the expression or `null` if the last statement
|
|
2146
|
+
* isn't an expression (or the code can't be parsed, in which case running
|
|
2147
|
+
* it will show the syntax error).
|
|
2148
|
+
*/
|
|
2149
|
+
function findLastExpression(code, blockType) {
|
|
2150
|
+
if (!window.acorn) return null;
|
|
2151
|
+
try {
|
|
2152
|
+
const {body} = acorn.parse(code, {
|
|
2153
|
+
ecmaVersion: 'latest',
|
|
2154
|
+
sourceType: blockType === 'module' ? 'module' : 'script',
|
|
2155
|
+
allowHashBang: true,
|
|
2156
|
+
});
|
|
2157
|
+
const last = body[body.length - 1];
|
|
2158
|
+
if (last?.type === 'ExpressionStatement' && !last.directive) {
|
|
2159
|
+
return [last.expression.start, last.expression.end];
|
|
1522
2160
|
}
|
|
1523
|
-
return groups;
|
|
1524
|
-
},
|
|
1525
|
-
[]
|
|
1526
|
-
);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
/**
|
|
1530
|
-
* Determines the header that should be displayed for a group of code and
|
|
1531
|
-
* whether or not the group's code should be hidden. A group is hidden if its
|
|
1532
|
-
* header starts with `hidePrefix`, in which case anything after the prefix (and
|
|
1533
|
-
* an optional colon) is used as the header.
|
|
1534
|
-
* @param {string} headerLines
|
|
1535
|
-
* @param {string} hidePrefix
|
|
1536
|
-
* @returns {{header: string, isHidden: boolean}}
|
|
1537
|
-
*/
|
|
1538
|
-
function parseGroupHeader(headerLines, hidePrefix) {
|
|
1539
|
-
const isHidden = !!hidePrefix && headerLines.startsWith(hidePrefix);
|
|
1540
|
-
return {
|
|
1541
|
-
header: isHidden
|
|
1542
|
-
? headerLines.slice(hidePrefix.length).replace(/^\s*:?\s*/, '')
|
|
1543
|
-
: headerLines,
|
|
1544
|
-
isHidden,
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
/**
|
|
1549
|
-
* Separates the hidden groups of code from the visible code.
|
|
1550
|
-
* @param {string} jsCode
|
|
1551
|
-
* @param {string} hidePrefix
|
|
1552
|
-
* @returns {{
|
|
1553
|
-
* visibleCode: string,
|
|
1554
|
-
* hiddenGroups: (ReturnType<parseJSCodeGroups>[number] & {runCount: number})[]
|
|
1555
|
-
* }}
|
|
1556
|
-
* `visibleCode` is the code to show in the editor. Each hidden group has a
|
|
1557
|
-
* `runCount` indicating how many visible groups must be run before it runs.
|
|
1558
|
-
*/
|
|
1559
|
-
function extractHiddenGroups(jsCode, hidePrefix) {
|
|
1560
|
-
const visibleGroups = [];
|
|
1561
|
-
const hiddenGroups = [];
|
|
1562
|
-
for (const group of parseJSCodeGroups(jsCode)) {
|
|
1563
|
-
if (parseGroupHeader(group.headerLines, hidePrefix).isHidden) {
|
|
1564
|
-
hiddenGroups.push({...group, runCount: visibleGroups.length});
|
|
1565
2161
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
2162
|
+
catch (e) {}
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/**
|
|
2167
|
+
* Joins groups of code back together making sure that each one (other than
|
|
2168
|
+
* the first) starts with a header so that they stay separate groups.
|
|
2169
|
+
* @param {string[]} blocks
|
|
2170
|
+
* @returns {string}
|
|
2171
|
+
*/
|
|
2172
|
+
function joinCodeBlocks(blocks) {
|
|
2173
|
+
return blocks
|
|
2174
|
+
.map(block => block.replace(/^(\s*[\r\n])+|\s+$/g, ''))
|
|
2175
|
+
.filter(Boolean)
|
|
2176
|
+
.map((block, index) => {
|
|
2177
|
+
const firstLine = block.split(/\r\n|\r|\n/)[0];
|
|
2178
|
+
return index && !/^\/\/[^]*\\\\$/.test(firstLine) ? `// \\\\\n${block}` : block;
|
|
2179
|
+
})
|
|
2180
|
+
.join('\n\n');
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Builds the HTML for a console that uses the given code and data attributes.
|
|
2185
|
+
* @param {Object} options
|
|
2186
|
+
* @param {string} options.code
|
|
2187
|
+
* @param {{[key: string]: string}} options.dataset
|
|
2188
|
+
* @param {{name: string, version: string}} options.packageInfo
|
|
2189
|
+
* @param {boolean} options.isFullPage
|
|
2190
|
+
* If `true` a full page is returned, otherwise just a snippet to embed.
|
|
2191
|
+
* @returns {string}
|
|
2192
|
+
*/
|
|
2193
|
+
function buildConsoleHtml({code, dataset, packageInfo, isFullPage}) {
|
|
2194
|
+
const {name, version} = packageInfo;
|
|
2195
|
+
const src = `https://cdn.jsdelivr.net/npm/${name}@${version.split('.')[0]}/dist/${name}.min.js`;
|
|
2196
|
+
const attrs = Object.entries(dataset)
|
|
2197
|
+
.map(([key, value]) => ` data-${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}="${escapeHtmlAttribute(value)}"`)
|
|
2198
|
+
.join('');
|
|
2199
|
+
const indent = isFullPage ? ' ' : ' ';
|
|
2200
|
+
|
|
2201
|
+
// Keeps the code from ending the script tag early.
|
|
2202
|
+
const safeCode = code.replace(/<\/(script)/gi, '<\\/$1').replace(/<!--/g, '<\\!--');
|
|
2203
|
+
const scriptLines = safeCode.trim()
|
|
2204
|
+
? [
|
|
2205
|
+
`<script src="${src}"${attrs}>`,
|
|
2206
|
+
...safeCode.split('\n').map(line => line.trim() ? ' ' + line : ''),
|
|
2207
|
+
'</script>',
|
|
2208
|
+
]
|
|
2209
|
+
: [`<script src="${src}"${attrs}></script>`];
|
|
2210
|
+
const indentedScript = scriptLines.map(line => line ? indent + line : line).join('\n');
|
|
2211
|
+
|
|
2212
|
+
return isFullPage
|
|
2213
|
+
? [
|
|
2214
|
+
'<!DOCTYPE html>',
|
|
2215
|
+
'<html lang="en">',
|
|
2216
|
+
' <head>',
|
|
2217
|
+
' <meta charset="utf-8">',
|
|
2218
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
2219
|
+
' <title>JS Box</title>',
|
|
2220
|
+
' <style>',
|
|
2221
|
+
' html, body { height: 100%; margin: 0; }',
|
|
2222
|
+
' </style>',
|
|
2223
|
+
' </head>',
|
|
2224
|
+
' <body>',
|
|
2225
|
+
indentedScript,
|
|
2226
|
+
' </body>',
|
|
2227
|
+
'</html>',
|
|
2228
|
+
'',
|
|
2229
|
+
].join('\n')
|
|
2230
|
+
: `<div style="height: 400px;">\n${indentedScript}\n</div>\n`;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* @param {string} value
|
|
2235
|
+
* @returns {string}
|
|
2236
|
+
*/
|
|
2237
|
+
function escapeHtmlAttribute(value) {
|
|
2238
|
+
return `${value}`.replace(/[&"<>]/g, c => `&#${c.charCodeAt(0)};`);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Finds the minimum indentation of all of the lines that have non-space
|
|
2243
|
+
* characters and removes the indentation accordingly for all indented lines.
|
|
2244
|
+
* @param {string} text
|
|
2245
|
+
* The string containing the lines of text that should be unindented.
|
|
2246
|
+
* @param {{trim: boolean, tabSize: number}=} opt_options
|
|
2247
|
+
* Optional, defaults to `{trim: true, tabSize: 4}`. The `trim` property
|
|
2248
|
+
* indicates if leading lines should be removed along with trailing
|
|
2249
|
+
* whitespaces. The `tabSize` property indicates how many spaces will be
|
|
2250
|
+
* used to replace all tab characters.
|
|
2251
|
+
* @returns {string}
|
|
2252
|
+
* A new version of `text` with all of the minimally indented lines having
|
|
2253
|
+
* no leading spacing and all other indented lines following suit. If
|
|
2254
|
+
* `opt_options.trim` is `true` all leading lines and trailing spaces will
|
|
2255
|
+
* not exist. All tab characters will be replaced with
|
|
2256
|
+
* `opt_options.tabSize` amount of space characters.
|
|
2257
|
+
*/
|
|
2258
|
+
function unindentMin(text, opt_options) {
|
|
2259
|
+
opt_options = Object(opt_options);
|
|
2260
|
+
const tabSize = opt_options.tabSize ?? 4;
|
|
2261
|
+
const trim = opt_options.trim ?? true;
|
|
2262
|
+
text = text.replace(/\t/g, ' '.repeat(tabSize));
|
|
2263
|
+
if (!/(^|[\r\n])\S/.test(text)) {
|
|
2264
|
+
const rgx = /(^|[\r\n])((?:(?!\r|\n)\s)+)(?=(\S)?)/g;
|
|
2265
|
+
let min = Infinity;
|
|
2266
|
+
for (let match; match = rgx.exec(text);) {
|
|
2267
|
+
if (match[3]) {
|
|
2268
|
+
min = Math.min(min, match[2].length);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
text = text.replace(
|
|
2272
|
+
rgx,
|
|
2273
|
+
(_, start, spaces) => start + spaces.slice(min)
|
|
2274
|
+
);
|
|
1568
2275
|
}
|
|
2276
|
+
return trim ? text.replace(/^(\s*[\r\n]+)+|\s+$/g, '') : text;
|
|
1569
2277
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
const src = `https://cdn.jsdelivr.net/npm/${name}@${version.split('.')[0]}/dist/${name}.min.js`;
|
|
1606
|
-
const attrs = Object.entries(dataset)
|
|
1607
|
-
.map(([key, value]) => ` data-${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}="${escapeHtmlAttribute(value)}"`)
|
|
1608
|
-
.join('');
|
|
1609
|
-
const indent = isFullPage ? ' ' : ' ';
|
|
1610
|
-
|
|
1611
|
-
// Keeps the code from ending the script tag early.
|
|
1612
|
-
const safeCode = code.replace(/<\/(script)/gi, '<\\/$1').replace(/<!--/g, '<\\!--');
|
|
1613
|
-
const scriptLines = safeCode.trim()
|
|
1614
|
-
? [
|
|
1615
|
-
`<script src="${src}"${attrs}>`,
|
|
1616
|
-
...safeCode.split('\n').map(line => line.trim() ? ' ' + line : ''),
|
|
1617
|
-
'</script>',
|
|
1618
|
-
]
|
|
1619
|
-
: [`<script src="${src}"${attrs}></script>`];
|
|
1620
|
-
const indentedScript = scriptLines.map(line => line ? indent + line : line).join('\n');
|
|
1621
|
-
|
|
1622
|
-
return isFullPage
|
|
1623
|
-
? [
|
|
1624
|
-
'<!DOCTYPE html>',
|
|
1625
|
-
'<html lang="en">',
|
|
1626
|
-
' <head>',
|
|
1627
|
-
' <meta charset="utf-8">',
|
|
1628
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
1629
|
-
' <title>JS Box</title>',
|
|
1630
|
-
' <style>',
|
|
1631
|
-
' html, body { height: 100%; margin: 0; }',
|
|
1632
|
-
' </style>',
|
|
1633
|
-
' </head>',
|
|
1634
|
-
' <body>',
|
|
1635
|
-
indentedScript,
|
|
1636
|
-
' </body>',
|
|
1637
|
-
'</html>',
|
|
1638
|
-
'',
|
|
1639
|
-
].join('\n')
|
|
1640
|
-
: `<div style="height: 400px;">\n${indentedScript}\n</div>\n`;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
/**
|
|
1644
|
-
* @param {string} value
|
|
1645
|
-
* @returns {string}
|
|
1646
|
-
*/
|
|
1647
|
-
function escapeHtmlAttribute(value) {
|
|
1648
|
-
return `${value}`.replace(/[&"<>]/g, c => `&#${c.charCodeAt(0)};`);
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
/**
|
|
1652
|
-
* Finds the minimum indentation of all of the lines that have non-space
|
|
1653
|
-
* characters and removes the indentation accordingly for all indented lines.
|
|
1654
|
-
* @param {string} text
|
|
1655
|
-
* The string containing the lines of text that should be unindented.
|
|
1656
|
-
* @param {{trim: boolean, tabSize: number}=} opt_options
|
|
1657
|
-
* Optional, defaults to `{trim: true, tabSize: 4}`. The `trim` property
|
|
1658
|
-
* indicates if leading lines should be removed along with trailing
|
|
1659
|
-
* whitespaces. The `tabSize` property indicates how many spaces will be
|
|
1660
|
-
* used to replace all tab characters.
|
|
1661
|
-
* @returns {string}
|
|
1662
|
-
* A new version of `text` with all of the minimally indented lines having
|
|
1663
|
-
* no leading spacing and all other indented lines following suit. If
|
|
1664
|
-
* `opt_options.trim` is `true` all leading lines and trailing spaces will
|
|
1665
|
-
* not exist. All tab characters will be replaced with
|
|
1666
|
-
* `opt_options.tabSize` amount of space characters.
|
|
1667
|
-
*/
|
|
1668
|
-
function unindentMin(text, opt_options) {
|
|
1669
|
-
opt_options = Object(opt_options);
|
|
1670
|
-
const tabSize = opt_options.tabSize ?? 4;
|
|
1671
|
-
const trim = opt_options.trim ?? true;
|
|
1672
|
-
text = text.replace(/\t/g, ' '.repeat(tabSize));
|
|
1673
|
-
if (!/(^|[\r\n])\S/.test(text)) {
|
|
1674
|
-
const rgx = /(^|[\r\n])((?:(?!\r|\n)\s)+)(?=(\S)?)/g;
|
|
1675
|
-
let min = Infinity;
|
|
1676
|
-
for (let match; match = rgx.exec(text);) {
|
|
1677
|
-
if (match[3]) {
|
|
1678
|
-
min = Math.min(min, match[2].length);
|
|
2278
|
+
|
|
2279
|
+
/**
|
|
2280
|
+
* Picks the properties of a description sent by the runner so that nothing
|
|
2281
|
+
* unexpected makes its way into the displays.
|
|
2282
|
+
* @param {*} description
|
|
2283
|
+
*/
|
|
2284
|
+
function sanitizeDescription(description) {
|
|
2285
|
+
description = Object(description);
|
|
2286
|
+
return {
|
|
2287
|
+
typeName: `${description.typeName}`,
|
|
2288
|
+
isPrimitive: !!description.isPrimitive,
|
|
2289
|
+
// Each part is [kind, text] where kind is used in a class name.
|
|
2290
|
+
parts: Array.from(description.parts ?? [], part => [
|
|
2291
|
+
/^[a-z]+$/.test(Object(part)[0]) ? part[0] : 'text',
|
|
2292
|
+
`${Object(part)[1]}`,
|
|
2293
|
+
]),
|
|
2294
|
+
...sanitizeEntries(description),
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* Picks the entries of a description sent by the runner.
|
|
2300
|
+
* @param {*} description
|
|
2301
|
+
*/
|
|
2302
|
+
function sanitizeEntries(description) {
|
|
2303
|
+
description = Object(description);
|
|
2304
|
+
const result = {};
|
|
2305
|
+
for (const key of ['entries', 'protoEntries']) {
|
|
2306
|
+
if (Array.isArray(description[key])) {
|
|
2307
|
+
// Each entry is [key, description, isEnumerable].
|
|
2308
|
+
result[key] = description[key].map(entry => [
|
|
2309
|
+
`${Object(entry)[0]}`,
|
|
2310
|
+
sanitizeDescription(Object(entry)[1]),
|
|
2311
|
+
Object(entry)[2] !== false,
|
|
2312
|
+
]);
|
|
1679
2313
|
}
|
|
1680
2314
|
}
|
|
1681
|
-
|
|
1682
|
-
rgx,
|
|
1683
|
-
(_, start, spaces) => start + spaces.slice(min)
|
|
1684
|
-
);
|
|
2315
|
+
return result;
|
|
1685
2316
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2317
|
+
|
|
2318
|
+
/**
|
|
2319
|
+
* NOTE: Called via main by the runner.
|
|
2320
|
+
* @param {object} options
|
|
2321
|
+
* @param {string} options.logId
|
|
2322
|
+
* @param {string} options.key
|
|
2323
|
+
* @param {any[]} options.descriptions
|
|
2324
|
+
* @param {{headers: string[], rows: {index: string, cells: any[]}[]}|null} options.table
|
|
2325
|
+
*/
|
|
2326
|
+
function appendLog({logId, key, descriptions, table, groupIds, groupId, isCollapsed, stack}) {
|
|
2327
|
+
key = `${key}`;
|
|
2328
|
+
mountedApp.displays.push({
|
|
2329
|
+
type: 'log',
|
|
2330
|
+
key,
|
|
2331
|
+
name: key === 'result' ? 'Result' : `console.${key}`,
|
|
2332
|
+
descriptions: Array.from(descriptions, sanitizeDescription),
|
|
2333
|
+
table: table ? {
|
|
2334
|
+
headers: Array.from(table.headers, h => `${h}`),
|
|
2335
|
+
rows: Array.from(table.rows, row => ({
|
|
2336
|
+
index: `${row.index}`,
|
|
2337
|
+
cells: Array.from(row.cells, cell => cell && sanitizeDescription(cell)),
|
|
2338
|
+
})),
|
|
2339
|
+
} : null,
|
|
2340
|
+
// The groups (from console.group()) that this message is in.
|
|
2341
|
+
groupIds: Array.from(groupIds ?? [], id => `${id}`),
|
|
2342
|
+
// Set if this message starts a group.
|
|
2343
|
+
groupId: groupId != null ? `${groupId}` : null,
|
|
2344
|
+
isCollapsed: !!isCollapsed,
|
|
2345
|
+
stack: stack != null ? `${stack}` : null,
|
|
2346
|
+
logId: `${logId}`,
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/**
|
|
2351
|
+
* NOTE: Called via main by the runner.
|
|
2352
|
+
* @param {object} options
|
|
2353
|
+
* @param {string} options.message
|
|
2354
|
+
* @param {number=} options.line
|
|
2355
|
+
* @param {number=} options.column
|
|
2356
|
+
*/
|
|
2357
|
+
function appendError({message, line, column}) {
|
|
2358
|
+
mountedApp.displays.push({
|
|
2359
|
+
type: 'error',
|
|
2360
|
+
message: `${message}`,
|
|
2361
|
+
line: +line,
|
|
2362
|
+
column: +column,
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/**
|
|
2367
|
+
* NOTE: Called via main by the runner.
|
|
2368
|
+
* @param {(string|number)[]} path
|
|
2369
|
+
* The log ID, the argument index and then pairs of entry group keys and entry
|
|
2370
|
+
* indices.
|
|
2371
|
+
* @param {*} description
|
|
2372
|
+
*/
|
|
2373
|
+
function updateDescriptionFor(path, description) {
|
|
2374
|
+
path = Array.from(path);
|
|
2375
|
+
const logId = path.shift();
|
|
2376
|
+
const display = mountedApp.displays.find(d => d.type === 'log' && d.logId === logId);
|
|
2377
|
+
let level = display?.descriptions[toIndex(path.shift())];
|
|
2378
|
+
|
|
2379
|
+
for (let i = 0; level && i < path.length; i += 2) {
|
|
2380
|
+
const groupKey = path[i];
|
|
2381
|
+
if (groupKey !== 'entries' && groupKey !== 'protoEntries') return;
|
|
2382
|
+
level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
|
|
1723
2383
|
}
|
|
2384
|
+
|
|
2385
|
+
if (level) Object.assign(level, sanitizeEntries(description));
|
|
1724
2386
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
* @param {string} options.key
|
|
1733
|
-
* @param {any[]} options.descriptions
|
|
1734
|
-
* @param {{headers: string[], rows: {index: string, cells: any[]}[]}|null} options.table
|
|
1735
|
-
*/
|
|
1736
|
-
function appendLog({logId, key, descriptions, table}) {
|
|
1737
|
-
key = `${key}`;
|
|
1738
|
-
mountedApp.displays.push({
|
|
1739
|
-
type: 'log',
|
|
1740
|
-
key,
|
|
1741
|
-
name: `console.${key}`,
|
|
1742
|
-
descriptions: Array.from(descriptions, sanitizeDescription),
|
|
1743
|
-
table: table ? {
|
|
1744
|
-
headers: Array.from(table.headers, h => `${h}`),
|
|
1745
|
-
rows: Array.from(table.rows, row => ({
|
|
1746
|
-
index: `${row.index}`,
|
|
1747
|
-
cells: Array.from(row.cells, cell => cell && sanitizeDescription(cell)),
|
|
1748
|
-
})),
|
|
1749
|
-
} : null,
|
|
1750
|
-
logId: `${logId}`,
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
/**
|
|
1755
|
-
* NOTE: Called via main by the runner.
|
|
1756
|
-
* @param {object} options
|
|
1757
|
-
* @param {string} options.message
|
|
1758
|
-
* @param {number=} options.line
|
|
1759
|
-
* @param {number=} options.column
|
|
1760
|
-
*/
|
|
1761
|
-
function appendError({message, line, column}) {
|
|
1762
|
-
mountedApp.displays.push({
|
|
1763
|
-
type: 'error',
|
|
1764
|
-
message: `${message}`,
|
|
1765
|
-
line: +line,
|
|
1766
|
-
column: +column,
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
/**
|
|
1771
|
-
* NOTE: Called via main by the runner.
|
|
1772
|
-
* @param {(string|number)[]} path
|
|
1773
|
-
* The log ID, the argument index and then pairs of entry group keys and entry
|
|
1774
|
-
* indices.
|
|
1775
|
-
* @param {*} description
|
|
1776
|
-
*/
|
|
1777
|
-
function updateDescriptionFor(path, description) {
|
|
1778
|
-
path = Array.from(path);
|
|
1779
|
-
const logId = path.shift();
|
|
1780
|
-
const display = mountedApp.displays.find(d => d.type === 'log' && d.logId === logId);
|
|
1781
|
-
let level = display?.descriptions[toIndex(path.shift())];
|
|
1782
|
-
|
|
1783
|
-
for (let i = 0; level && i < path.length; i += 2) {
|
|
1784
|
-
const groupKey = path[i];
|
|
1785
|
-
if (groupKey !== 'entries' && groupKey !== 'protoEntries') return;
|
|
1786
|
-
level = level[groupKey]?.[toIndex(path[i + 1])]?.[1];
|
|
2387
|
+
|
|
2388
|
+
/**
|
|
2389
|
+
* NOTE: Called via main by the runner once it has run a group of code.
|
|
2390
|
+
*/
|
|
2391
|
+
function onCodeRan() {
|
|
2392
|
+
mountedApp.runningCount = Math.max(0, mountedApp.runningCount - 1);
|
|
2393
|
+
mountedApp.runHiddenGroups();
|
|
1787
2394
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* NOTE: Called by main when the console is popped out into a separate window
|
|
2398
|
+
* or brought back into the page.
|
|
2399
|
+
* @param {boolean} isPoppedOut
|
|
2400
|
+
* @param {*=} state
|
|
2401
|
+
* When brought back, the state of the pop-out window's viewer (if it sent
|
|
2402
|
+
* it).
|
|
2403
|
+
*/
|
|
2404
|
+
function setPoppedOut(isPoppedOut, state) {
|
|
2405
|
+
mountedApp.isPoppedOut = isPoppedOut;
|
|
2406
|
+
if (state) mountedApp.restoreState(state);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
/**
|
|
2410
|
+
* NOTE: Called by main to ask the pop-out window to send its state back.
|
|
2411
|
+
*/
|
|
2412
|
+
function popIn() {
|
|
2413
|
+
mountedApp.bringBack();
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/**
|
|
2417
|
+
* NOTE: Called by main if the browser blocked the pop-out window.
|
|
2418
|
+
*/
|
|
2419
|
+
function onPopOutFailed() {
|
|
2420
|
+
mountedApp.displays.push({
|
|
2421
|
+
type: 'notice',
|
|
2422
|
+
message: 'The pop-out window was blocked by the browser. Allow pop-ups for this site and try again.',
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* NOTE: Called via main by the runner when console.clear() is called.
|
|
2428
|
+
*/
|
|
2429
|
+
function clearDisplays() {
|
|
2430
|
+
mountedApp.displays = [{type: 'notice', message: 'Console was cleared'}];
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Only allows non-negative integers to be used as an index.
|
|
2435
|
+
* @param {*} value
|
|
2436
|
+
* @returns {number|undefined}
|
|
2437
|
+
*/
|
|
2438
|
+
function toIndex(value) {
|
|
2439
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
},
|
|
2443
|
+
// Exact versions are used so that a new release of a library can never
|
|
2444
|
+
// change how an existing version of this console works. Ace and Prism
|
|
2445
|
+
// load other files (eg. language modes) from next to these files.
|
|
2446
|
+
jsUrls: [
|
|
2447
|
+
libraryUrl('vue', 'dist/vue.global.prod.js'),
|
|
2448
|
+
libraryUrl('ace-builds', 'src-noconflict/ace.js'),
|
|
2449
|
+
libraryUrl('prismjs', 'components/prism-core.min.js'),
|
|
2450
|
+
libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
|
|
2451
|
+
libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
|
|
2452
|
+
// Used to find the last expression in each block of code so that its
|
|
2453
|
+
// value can be shown.
|
|
2454
|
+
...(showResults ? [libraryUrl('acorn', 'dist/acorn.js')] : []),
|
|
2455
|
+
],
|
|
2456
|
+
cssUrls: [
|
|
2457
|
+
'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
|
|
2458
|
+
libraryUrl('prism-themes', 'themes/prism-vsc-dark-plus.min.css'),
|
|
2459
|
+
libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.css'),
|
|
2460
|
+
],
|
|
2461
|
+
head: targetWindow ? '<meta charset="utf-8"><title>JS Box</title>' : '',
|
|
2462
|
+
htmlAttributes: 'data-theme="' + theme + '"',
|
|
2463
|
+
targetWindow,
|
|
2464
|
+
onMessage(message) {
|
|
2465
|
+
relayMessage(message.data, {runner, host: {apply: (func, args) => hostFuncs[func](...args)}});
|
|
2466
|
+
},
|
|
2467
|
+
async onReady() {
|
|
2468
|
+
// The dataset is passed as is so that the viewer knows which options
|
|
2469
|
+
// were actually specified (eg. when copying the console as HTML).
|
|
2470
|
+
this.call('init', script.textContent, dataset, {
|
|
2471
|
+
runnerMode,
|
|
2472
|
+
blockType,
|
|
2473
|
+
showResults,
|
|
2474
|
+
packageInfo: PACKAGE_INFO,
|
|
2475
|
+
libraryVersions: LIBRARY_VERSIONS,
|
|
2476
|
+
isPopOut: !!targetWindow,
|
|
2477
|
+
popOutState: state ?? null,
|
|
2478
|
+
});
|
|
2479
|
+
},
|
|
2480
|
+
body: VIEWER_IFRAME_HTML,
|
|
2481
|
+
style: {
|
|
2482
|
+
width: '100%',
|
|
2483
|
+
height: '100%',
|
|
2484
|
+
border: 0,
|
|
2485
|
+
display: 'block',
|
|
2486
|
+
},
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
1846
2489
|
|
|
1847
|
-
|
|
2490
|
+
inlineViewer = activeViewer = createViewer();
|
|
2491
|
+
script.parentNode.insertBefore(inlineViewer.iframe, script);
|
|
1848
2492
|
}
|
|
1849
2493
|
|
|
1850
2494
|
// NOTE: This solution was intentionally written without using newer JS
|
|
1851
2495
|
// features to make the minified version even smaller.
|
|
1852
2496
|
var createCallableFrame = (function () {
|
|
1853
2497
|
var IFRAME_SCRIPT_MESSAGE_CODE = parseFunction(function() {
|
|
2498
|
+
// The window that created this one: the opener if this is a pop-out
|
|
2499
|
+
// window or else the parent of the IFRAME.
|
|
2500
|
+
var HOST_WINDOW = window.opener || window.parent;
|
|
2501
|
+
|
|
1854
2502
|
// NOTE: Referencing with window to ensure that local namespace will not
|
|
1855
2503
|
// interfere.
|
|
1856
2504
|
window.addEventListener('message', function(e) {
|
|
1857
2505
|
var data = e.data;
|
|
1858
2506
|
if (
|
|
1859
|
-
e.source ===
|
|
2507
|
+
e.source === HOST_WINDOW
|
|
1860
2508
|
&& data && /^[A-Za-z_$][\w$]*$/.test(data.funcName)
|
|
1861
2509
|
&& Array.isArray(data.args)
|
|
1862
2510
|
) {
|
|
@@ -1866,7 +2514,7 @@
|
|
|
1866
2514
|
});
|
|
1867
2515
|
|
|
1868
2516
|
function messageParent(message) {
|
|
1869
|
-
|
|
2517
|
+
HOST_WINDOW.postMessage(message, '*');
|
|
1870
2518
|
}
|
|
1871
2519
|
}).body;
|
|
1872
2520
|
|
|
@@ -1892,6 +2540,9 @@
|
|
|
1892
2540
|
* The HTML code that will be used to instantiate the page.
|
|
1893
2541
|
* @param {string=} options.htmlAttributes
|
|
1894
2542
|
* Attributes to add to the `<html>` element of the IFRAME.
|
|
2543
|
+
* @param {Window=} options.targetWindow
|
|
2544
|
+
* If given the page is written into this (same origin) window (eg. a
|
|
2545
|
+
* pop-out window) instead of into a new IFRAME.
|
|
1895
2546
|
* @param {((this: R, event: MessageEvent) => void)=} options.onMessage
|
|
1896
2547
|
* @param {((this: R, event: MessageEvent) => void)=} options.onReady
|
|
1897
2548
|
* @returns {R}
|
|
@@ -1916,11 +2567,11 @@
|
|
|
1916
2567
|
'(function(READY_ID){',
|
|
1917
2568
|
parseFunction(function() {
|
|
1918
2569
|
applyParent = function(funcName, args) {
|
|
1919
|
-
|
|
2570
|
+
HOST_WINDOW.postMessage({funcName: funcName, args: args, id: READY_ID}, '*');
|
|
1920
2571
|
};
|
|
1921
2572
|
|
|
1922
2573
|
callParent = function(funcName) {
|
|
1923
|
-
|
|
2574
|
+
HOST_WINDOW.postMessage(
|
|
1924
2575
|
{funcName: funcName, args: Array.prototype.slice.call(arguments, 1), id: READY_ID},
|
|
1925
2576
|
'*'
|
|
1926
2577
|
);
|
|
@@ -1940,8 +2591,12 @@
|
|
|
1940
2591
|
// Prevents the HTML parser from ending the inline script early.
|
|
1941
2592
|
.replace(/<(?=\/script|!--)/gi, '\\x3C');
|
|
1942
2593
|
|
|
1943
|
-
|
|
1944
|
-
var IFRAME = document.createElement('iframe');
|
|
2594
|
+
var TARGET_WINDOW = options.targetWindow;
|
|
2595
|
+
var IFRAME = TARGET_WINDOW ? null : document.createElement('iframe');
|
|
2596
|
+
function getFrameWindow() {
|
|
2597
|
+
return TARGET_WINDOW || IFRAME.contentWindow;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
1945
2600
|
var HTML_CODE = [
|
|
1946
2601
|
'<!DOCTYPE html>',
|
|
1947
2602
|
'<html ' + (options.htmlAttributes || '') + '>',
|
|
@@ -1962,10 +2617,20 @@
|
|
|
1962
2617
|
'</body>',
|
|
1963
2618
|
'</html>'
|
|
1964
2619
|
].join('\n');
|
|
1965
|
-
IFRAME.
|
|
2620
|
+
// Writes the page into the target window or sets the IFRAME's source.
|
|
2621
|
+
if (TARGET_WINDOW) {
|
|
2622
|
+
TARGET_WINDOW.document.open();
|
|
2623
|
+
TARGET_WINDOW.document.write(HTML_CODE);
|
|
2624
|
+
TARGET_WINDOW.document.close();
|
|
2625
|
+
}
|
|
2626
|
+
else {
|
|
2627
|
+
IFRAME.srcdoc = HTML_CODE;
|
|
2628
|
+
// Allows the console to go full screen.
|
|
2629
|
+
IFRAME.setAttribute('allow', 'fullscreen');
|
|
2630
|
+
}
|
|
1966
2631
|
|
|
1967
2632
|
// Set the style of the iframe.
|
|
1968
|
-
if (style) {
|
|
2633
|
+
if (style && IFRAME) {
|
|
1969
2634
|
if ('string' === typeof style) {
|
|
1970
2635
|
IFRAME.style.cssText = style;
|
|
1971
2636
|
}
|
|
@@ -1979,8 +2644,8 @@
|
|
|
1979
2644
|
}
|
|
1980
2645
|
|
|
1981
2646
|
// Adds an event listener so that messages from the IFRAME will be captured.
|
|
1982
|
-
|
|
1983
|
-
if (e.source ===
|
|
2647
|
+
function onWindowMessage(e) {
|
|
2648
|
+
if (e.source === getFrameWindow()) {
|
|
1984
2649
|
var data = e.data;
|
|
1985
2650
|
var dataIsReadyId = READY_ID === data;
|
|
1986
2651
|
if (isReady && !dataIsReadyId) {
|
|
@@ -2004,13 +2669,14 @@
|
|
|
2004
2669
|
console.warn('Message sent from callable frame prematurely:', e);
|
|
2005
2670
|
}
|
|
2006
2671
|
}
|
|
2007
|
-
}
|
|
2672
|
+
}
|
|
2673
|
+
addEventListener('message', onWindowMessage);
|
|
2008
2674
|
|
|
2009
2675
|
// Returns an object which makes it possible to call functions and get
|
|
2010
2676
|
// access to the IFRAME.
|
|
2011
2677
|
var queuedMessages = [];
|
|
2012
2678
|
function postToFrame(message) {
|
|
2013
|
-
if (isReady)
|
|
2679
|
+
if (isReady) getFrameWindow().postMessage(message, '*');
|
|
2014
2680
|
else queuedMessages.push(message);
|
|
2015
2681
|
}
|
|
2016
2682
|
var callableFrame = {
|
|
@@ -2020,7 +2686,12 @@
|
|
|
2020
2686
|
call: function(funcName) {
|
|
2021
2687
|
postToFrame({funcName: funcName, args: Array.prototype.slice.call(arguments, 1)});
|
|
2022
2688
|
},
|
|
2023
|
-
iframe: IFRAME
|
|
2689
|
+
iframe: IFRAME,
|
|
2690
|
+
window: TARGET_WINDOW,
|
|
2691
|
+
// Stops listening for messages from the frame.
|
|
2692
|
+
dispose: function() {
|
|
2693
|
+
removeEventListener('message', onWindowMessage);
|
|
2694
|
+
}
|
|
2024
2695
|
};
|
|
2025
2696
|
return callableFrame;
|
|
2026
2697
|
};
|
|
@@ -2028,7 +2699,9 @@
|
|
|
2028
2699
|
* @typedef {Object} createCallableFrame_Return
|
|
2029
2700
|
* @property {(funcName: string, args: any[]) => void} apply
|
|
2030
2701
|
* @property {(funcName: string, ...args: any[]) => void} call
|
|
2031
|
-
* @property {HTMLIFrameElement} iframe
|
|
2702
|
+
* @property {HTMLIFrameElement?} iframe
|
|
2703
|
+
* @property {Window=} window
|
|
2704
|
+
* @property {() => void} dispose
|
|
2032
2705
|
*/
|
|
2033
2706
|
|
|
2034
2707
|
/**
|