yourjs-box 1.3.1 → 1.5.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 -3
- package/dist/yourjs-box.full.js +408 -59
- package/dist/yourjs-box.js +187 -20
- 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.5.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}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.popped-out{align-items:center;background-color:var(--console-bg);color:var(--console-text);display:flex;inset:0;justify-content:center;position:fixed;z-index:950}.popped-out-content{align-items:center;display:flex;flex-direction:column;gap:8px;padding:16px;text-align:center}.popped-out-title{font-size:15px;font-weight:600;margin-top:8px}.popped-out-text{color:var(--muted-text);font-size:13px}@keyframes fade-in{from{opacity:0}}@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}.rotated-90deg{transform:rotate(90deg)}.align-top{vertical-align:top!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.no-select{-webkit-user-select:none;user-select:none}:root{color-scheme:light;--console-font:ui-monospace,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;--console-bg:#fff;--console-text:#1f1f1f;--row-border:#f0f0f0;--arrow:#727272;--chevron:#9aa0a6;--entry-key:#881391;--preview-key:#5f6368;--string:#c41a16;--number:#1a1aa6;--null:#80868b;--header-bg:#f1f3f4;--header-text:#3c4043;--warn-bg:#fffbe5;--warn-border:#fff5c2;--warn-text:#5c3c00;--warn-icon:#e8a600;--error-bg:#fff0f0;--error-border:#ffd6d6;--error-text:#dc362e;--error-icon:#dc362e;--table-header-bg:#f3f3f3;--table-border:#d0d0d0;--toolbar-bg:#f3f3f3;--toolbar-border:#d6d6d6;--toolbar-text:#333;--muted-text:#5f6368;--button-hover:rgb(0 0 0 / 0.08);--accent:#1a73e8;--accent-hover:#1765cc;--accent-text:#fff;--divider-bg:#f3f3f3;--divider-hover-bg:#e8eaed;--divider-grip:#b0b0b0;--dialog-bg:#fff;--backdrop:rgb(0 0 0 / 0.25)}:root[data-theme=dark]{color-scheme:dark;--console-bg:#242424;--console-text:#e3e3e3;--row-border:#3a3a3a;--arrow:#9aa0a6;--chevron:#80868b;--entry-key:#5db0d7;--preview-key:#9aa0a6;--string:#f28b54;--number:#9980ff;--null:#8e8e8e;--header-bg:#2d2e30;--header-text:#c4c7c5;--warn-bg:#332b00;--warn-border:#665500;--warn-text:#ffd17a;--warn-icon:#ffd17a;--error-bg:#290000;--error-border:#5c0000;--error-text:#ff8080;--error-icon:#ff6b6b;--table-header-bg:#2e2e2e;--table-border:#4a4a4a;--toolbar-bg:#2b2b2b;--toolbar-border:#474747;--toolbar-text:#e3e3e3;--muted-text:#9aa0a6;--button-hover:rgb(255 255 255 / 0.1);--accent:#8ab4f8;--accent-hover:#aecbfa;--accent-text:#202124;--divider-bg:#2b2b2b;--divider-hover-bg:#333;--divider-grip:#6b6b6b;--dialog-bg:#2d2e30;--backdrop:rgb(0 0 0 / 0.5)}#displays{background-color:var(--console-bg);color:var(--console-text)}#displays>div{font-family:var(--console-font);font-size:calc(12px * var(--text-scale, 1));line-height:calc(16px * var(--text-scale, 1))}.console-row{border-bottom:1px solid var(--row-border);padding:2px 8px 2px 24px;position:relative;white-space:pre-wrap;word-break:break-word}.console-row>.row-icon{left:6px;line-height:0;position:absolute;top:4px}.log-error,.log-warn{margin-top:-1px}.log-warn{background-color:var(--warn-bg);border-bottom-color:var(--warn-border);border-top:1px solid var(--warn-border);color:var(--warn-text)}.log-warn>.row-icon{color:var(--warn-icon)}.log-error{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text)}.log-error>.row-icon{color:var(--error-icon)}.console-row{padding-left:calc(24px + var(--depth,0) * 16px)}.console-row>.row-icon{left:calc(6px + var(--depth,0) * 16px)}.group-header{cursor:pointer;font-weight:700}.group-header>.row-icon.arrow{top:5px}.log-assert{background-color:var(--error-bg);border-bottom-color:var(--error-border);border-top:1px solid var(--error-border);color:var(--error-text);margin-top:-1px}.log-assert>.row-icon{color:var(--error-icon)}.log-result>.row-icon{color:var(--chevron)}.trace-stack{color:var(--null);padding-left:1ch}.splash-message{color:var(--muted-text);font-size:13px;max-width:280px;text-align:center}.splash-slow{animation:show-after-delay 0s 10s both}@keyframes show-after-delay{from{visibility:hidden}to{visibility:visible}}#splash.failed .splash-progress,#splash.failed .splash-slow,.splash-error{display:none}#splash.failed .splash-error{display:block}.code-header{background-color:var(--header-bg);border-bottom:1px solid var(--row-border);color:var(--header-text);font-weight:700;padding:2px 8px;white-space:pre-wrap}.code-header.toggleable{cursor:pointer}.code-row .copy-to-editor-button{align-items:center;background-color:var(--console-bg);border:1px solid var(--toolbar-border);border-radius:4px;color:var(--toolbar-text);cursor:pointer;display:inline-flex;height:22px;justify-content:center;opacity:0;padding:0;position:absolute;right:6px;top:2px;transition:opacity .15s;width:22px}.code-row .copy-to-editor-button:focus-visible,.code-row:hover .copy-to-editor-button{opacity:1}.code-row .copy-to-editor-button:hover{background-color:var(--toolbar-bg)}@media (hover:none){.code-row .copy-to-editor-button{opacity:.8}}.notice{color:var(--null);font-style:italic}.code-row>.row-icon{color:var(--chevron)}.code-row code,.code-row pre{background:0 0!important;font-family:var(--console-font)!important;font-size:calc(12px * var(--text-scale, 1))!important;line-height:calc(16px * var(--text-scale, 1))!important;padding:0!important;text-shadow:none!important}.js-value{max-width:100%;vertical-align:top}.row-content>.js-value+.js-value{margin-left:1ch}.js-value-header.expandable{cursor:default}.js-value .expansion{padding-left:12px}.js-value .loading{color:var(--null);padding-left:12px}.arrow{display:inline-block;height:10px;position:relative;width:12px}.arrow.expandable::before{border-color:transparent transparent transparent var(--arrow);border-style:solid;border-width:4px 0 4px 6px;content:'';left:2px;position:absolute;top:1px;transform-origin:3px 4px;transition:transform .1s}.arrow.expandable.expanded::before{transform:rotate(90deg)}.entry-key{color:var(--entry-key)}.entry-key.dim{opacity:.6}.t-key{color:var(--preview-key)}.t-regexp,.t-string,.t-symbol{color:var(--string)}.t-number{color:var(--number)}.t-null{color:var(--null)}.t-function{font-style:italic}.t-node{color:var(--entry-key)}.console-table{border:1px solid var(--table-border);border-collapse:collapse;margin:2px 0 4px;white-space:nowrap}.console-table td,.console-table th{border-left:1px solid var(--table-border);max-width:300px;overflow:hidden;padding:1px 4px;text-align:left;text-overflow:ellipsis}.console-table th{background-color:var(--table-header-bg);border-bottom:1px solid var(--table-border);font-weight:400}";
|
|
7
|
+
const VIEWER_IFRAME_CSS = "[v-cloak]{display:none}body,html{height:100%;margin:0}body{background-color:var(--console-bg);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}#vueApp{display:flex;flex-direction:column;inset:0;position:fixed}#main{display:grid;flex-grow:1;gap:0;min-height:0;position:relative}#main.is-moving-divider{-webkit-user-select:none;user-select:none}#main.col-orient.is-moving-divider{cursor:col-resize}#main.row-orient.is-moving-divider{cursor:row-resize}#main.is-moving-divider::after{background-color:var(--accent);content:'';position:absolute;z-index:99}#main.col-orient.is-moving-divider::after{bottom:0;left:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px);top:0;width:2px}#main.row-orient.is-moving-divider::after{height:2px;left:0;right:0;top:calc(100% - var(--temp-editor-pct) - var(--divider-size)/ 2 - 1px)}#main.col-orient{grid-template-columns:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2);grid-template-rows:1fr}#main.row-orient{grid-template-columns:1fr;grid-template-rows:1fr var(--divider-size) calc(var(--editor-pct) - var(--divider-size)/ 2)}.divider{background-color:var(--divider-bg);box-sizing:border-box;position:relative;transition:background-color .15s}#main.col-orient .divider{border-left:1px solid var(--toolbar-border);border-right:1px solid var(--toolbar-border);cursor:col-resize}#main.row-orient .divider{border-bottom:1px solid var(--toolbar-border);border-top:1px solid var(--toolbar-border);cursor:row-resize}.divider::after{background-color:var(--divider-grip);border-radius:2px;content:'';left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);transition:background-color .15s}#main.col-orient .divider::after{height:32px;width:2px}#main.row-orient .divider::after{height:2px;width:32px}#main.is-moving-divider .divider,.divider:hover{background-color:var(--divider-hover-bg)}#main.is-moving-divider .divider::after,.divider:hover::after{background-color:var(--accent)}#displays{position:relative}#displays>div{position:absolute;inset:0;overflow:auto}#bottomNav{align-items:center;background-color:var(--toolbar-bg);border-top:1px solid var(--toolbar-border);color:var(--toolbar-text);display:flex;flex:0 0 auto;font-size:12px;gap:8px;height:32px;padding:0 6px 0 8px;-webkit-user-select:none;user-select:none}#bottomNav>.brand{align-items:center;display:flex;flex-grow:1;gap:8px}.runner-badge{border:1px solid var(--toolbar-border);border-radius:999px;color:var(--muted-text);font-size:10px;letter-spacing:.04em;line-height:15px;padding:0 6px;text-transform:uppercase}#bottomNav>.buttons{align-items:center;display:flex;gap:2px}#bottomNav .separator{background-color:var(--toolbar-border);height:16px;margin:0 4px;width:1px}#bottomNav button{align-items:center;background:0 0;border:0;border-radius:4px;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:5px;height:24px;justify-content:center;min-width:26px;padding:0 6px}#bottomNav button>span:not(.label){display:inline-flex;font-size:14px}#bottomNav button svg:not(.spin){transform:none!important}#bottomNav button:hover:not(:disabled){background-color:var(--button-hover)}#bottomNav button:disabled{cursor:not-allowed;opacity:.45}#bottomNav button.primary{background-color:var(--accent);color:var(--accent-text);font-weight:600;margin-left:2px;padding:0 10px 0 8px}#bottomNav button.primary>span:not(.label){font-size:11px}#bottomNav button.primary:hover:not(:disabled){background-color:var(--accent-hover)}#bottomNav button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.spin{animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.logo{align-items:center;color:var(--toolbar-text);display:inline-flex;font-weight:700;gap:5px;letter-spacing:-.01em;line-height:1}.logo-mark{align-items:flex-end;background-color:#f7df1e;border-radius:3px;box-sizing:border-box;color:#1a1a1a;display:inline-flex;font-size:9.5px;font-weight:800;height:18px;justify-content:flex-end;letter-spacing:-.02em;padding:0 2px 2px 0;width:18px}.logo-text{font-size:14px}.logo-large{gap:12px}.logo-large>.logo-mark{border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .18);font-size:24px;height:56px;padding:0 6px 5px 0;width:56px}.logo-large>.logo-text{font-size:36px}#splash{align-items:center;background-color:var(--console-bg);display:flex;inset:0;justify-content:center;position:fixed;transition:opacity .35s ease,visibility .35s;z-index:1000}#splash.hidden{opacity:0;visibility:hidden}.splash-content{align-items:center;animation:splash-in .4s ease-out both;display:flex;flex-direction:column;gap:24px}.splash-progress{background-color:var(--toolbar-border);border-radius:3px;height:3px;overflow:hidden;width:140px}.splash-progress>div{animation:splash-progress 1.1s ease-in-out infinite;background-color:var(--accent);border-radius:inherit;height:100%;width:40%}@keyframes splash-in{from{opacity:0;transform:translateY(6px)}}@keyframes splash-progress{from{transform:translateX(-100%)}to{transform:translateX(250%)}}.dialog-backdrop{align-items:center;animation:fade-in .12s ease-out;background-color:var(--backdrop);display:flex;inset:0;justify-content:center;position:fixed;z-index:900}.dialog{animation:dialog-in .15s ease-out;background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;box-shadow:0 12px 40px rgb(0 0 0 / .3);color:var(--console-text);font-size:13px;padding:16px;width:min(360px,calc(100% - 32px))}.dialog-title{font-size:14px;font-weight:600;margin-bottom:6px}.dialog-message{color:var(--muted-text);line-height:1.45}.dialog-buttons{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.dialog-button{background:0 0;border:1px solid var(--toolbar-border);border-radius:4px;color:inherit;cursor:pointer;font:inherit;height:28px;padding:0 12px}.dialog-button:hover{background-color:var(--button-hover)}.dialog-button.primary{background-color:var(--accent);border-color:var(--accent);color:var(--accent-text);font-weight:600}.dialog-button.primary:hover{background-color:var(--accent-hover)}.dialog-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}#bottomNav button.logo-button{margin-left:-4px;padding:0 4px}.about-dialog{display:flex;flex-direction:column;max-height:calc(100% - 24px);padding:0;width:min(620px,calc(100% - 24px))}.about-dialog.is-export{height:min(560px,calc(100% - 24px))}.about-header{align-items:center;border-bottom:1px solid var(--toolbar-border);display:flex;flex:0 0 auto;padding:0 8px 0 12px}.tabs{display:flex;flex-grow:1;gap:4px}.tab{background:0 0;border:0;border-bottom:2px solid transparent;color:var(--muted-text);cursor:pointer;font:inherit;font-weight:600;padding:10px 8px 8px}.tab:hover{color:var(--console-text)}.tab.active{border-bottom-color:var(--accent);color:var(--console-text)}.close-button{align-items:center;background:0 0;border:0;border-radius:4px;color:var(--muted-text);cursor:pointer;display:inline-flex;font-size:16px;height:28px;justify-content:center;width:28px}.close-button:hover{background-color:var(--button-hover);color:var(--console-text)}.close-button:focus-visible,.segmented>button:focus-visible,.tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.about-body{flex:1 1 auto;min-height:0;overflow:auto;padding:16px}.about-title{align-items:center;display:flex;gap:12px}.logo-medium>.logo-mark{border-radius:7px;font-size:16px;height:40px;padding:0 4px 4px 0;width:40px}.logo-medium>.logo-text{display:none}.about-name{font-size:17px;font-weight:700}.about-aka{color:var(--muted-text);font-size:13px;font-weight:400;margin-left:4px}.about-version{color:var(--muted-text);font-size:12px}.about-description{line-height:1.5;margin:12px 0}.about-links{display:flex;flex-wrap:wrap;gap:6px 16px}.about-links a{color:var(--accent);font-weight:600;text-decoration:none}.about-links a:hover{text-decoration:underline}.about-body h3{color:var(--muted-text);font-size:11px;letter-spacing:.05em;margin:18px 0 6px;text-transform:uppercase}.about-details{display:grid;gap:4px 16px;grid-template-columns:max-content 1fr;margin:0}.about-details dt{color:var(--muted-text)}.about-details dd{margin:0}.about-details kbd{background-color:var(--toolbar-bg);border:1px solid var(--toolbar-border);border-bottom-width:2px;border-radius:4px;font-family:inherit;font-size:11px;padding:0 4px}.about-footer{border-top:1px solid var(--toolbar-border);color:var(--muted-text);font-size:12px;margin-top:18px;padding-top:12px}.export-body{display:flex;flex-direction:column;gap:10px}.export-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between}.segmented{border:1px solid var(--toolbar-border);border-radius:6px;display:inline-flex;overflow:hidden}.segmented>button{background:0 0;border:0;color:var(--muted-text);cursor:pointer;font:inherit;font-size:12px;font-weight:600;padding:5px 10px}.segmented>button+button{border-left:1px solid var(--toolbar-border)}.segmented>button:hover{background-color:var(--button-hover)}.segmented>button.active{background-color:var(--accent);color:var(--accent-text)}.export-note{color:var(--muted-text);font-size:12px}.export-preview{border:1px solid var(--toolbar-border);border-radius:6px;flex:1 1 auto;min-height:120px;overflow:hidden}.export-actions{display:flex;gap:8px;justify-content:flex-end}.file-input{display:none}.more-menu{background-color:var(--dialog-bg);border:1px solid var(--toolbar-border);border-radius:8px;bottom:38px;box-shadow:0 8px 28px rgb(0 0 0 / .25);color:var(--console-text);font-size:13px;max-height:calc(100% - 48px);min-width:236px;overflow:auto;padding:4px;position:absolute;right:6px;z-index:800}.menu-item{align-items:center;background:0 0;border:0;border-radius:5px;color:inherit;cursor:pointer;display:flex;font:inherit;gap:8px;padding:6px 8px;text-align:left;width:100%}.menu-item:focus-visible,.menu-item:hover:not(:disabled){background-color:var(--button-hover);outline:0}.menu-item:disabled{cursor:not-allowed;opacity:.4}.menu-icon{color:var(--muted-text);display:inline-flex;font-size:14px;justify-content:center;width:16px}.menu-icon svg{transform:none!important}.menu-row{align-items:center;display:flex;gap:2px;padding:2px 2px 2px 8px}.menu-row-label{flex-grow:1}.menu-step{font-weight:600;justify-content:center;width:auto}.menu-percent{color:var(--muted-text);font-variant-numeric:tabular-nums;font-weight:400;min-width:52px}.menu-separator{background-color:var(--toolbar-border);height:1px;margin:4px 0}.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.5.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
|
|
13
13
|
/**
|
|
14
14
|
* Viewer IFRAME's HTML code
|
|
15
15
|
* @type {string}
|
|
16
16
|
*/
|
|
17
|
-
const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading…</div><div class=\"splash-message splash-error\">JS Box couldn’t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' && !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf && bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A−</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License © 2023-present Christopher West · Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
|
|
17
|
+
const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading…</div><div class=\"splash-message splash-error\">JS Box couldn’t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' && !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf && bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A−</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save…</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License © 2023-present Christopher West · Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
|
|
18
18
|
/**
|
|
19
19
|
* The URL of this script which is used to remove this script's lines from the
|
|
20
20
|
* stack traces of errors in window mode.
|
|
@@ -62,6 +62,27 @@
|
|
|
62
62
|
return new URL(baseUrl + path, document.baseURI).href;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Where packages imported by name (eg. `import _ from 'lodash'`) are loaded
|
|
67
|
+
* from unless data-imports-url is given. `{specifier}` is replaced with what
|
|
68
|
+
* was imported (eg. "lodash@4/fp").
|
|
69
|
+
*/
|
|
70
|
+
const DEFAULT_IMPORTS_URL = 'https://esm.sh/{specifier}';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Makes the part of a URL template before its first placeholder absolute
|
|
74
|
+
* (relative to the page) because the code runs somewhere (eg. a worker)
|
|
75
|
+
* that doesn't know the page's URL.
|
|
76
|
+
* @param {string} template
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function resolveUrlTemplate(template) {
|
|
80
|
+
const index = template.indexOf('{');
|
|
81
|
+
const prefix = index < 0 ? template : template.slice(0, index);
|
|
82
|
+
return new URL(prefix || './', document.baseURI).href.replace(/\/$/, prefix.endsWith('/') || !prefix ? '/' : '')
|
|
83
|
+
+ (index < 0 ? '' : template.slice(index));
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
/**
|
|
66
87
|
* The only functions that may be relayed to the viewer and to the runner.
|
|
67
88
|
* The runner executes the user's code so anything it sends must be limited
|
|
@@ -130,6 +151,9 @@
|
|
|
130
151
|
const RESULT_KEY = `yourjs-box.result.${Math.random().toString(36).slice(2)}`;
|
|
131
152
|
globalThis[Symbol.for(RESULT_KEY)] = reportResult;
|
|
132
153
|
|
|
154
|
+
// The console functions that were replaced so that they can be restored.
|
|
155
|
+
const consoleWrappers = [];
|
|
156
|
+
|
|
133
157
|
// Overrides for console functions. In window mode this also captures
|
|
134
158
|
// anything else that the page logs, just like the browser's console.
|
|
135
159
|
for (const key of [
|
|
@@ -139,12 +163,14 @@
|
|
|
139
163
|
]) {
|
|
140
164
|
const original = console[key];
|
|
141
165
|
if ('function' !== typeof original) continue;
|
|
142
|
-
|
|
166
|
+
const wrapper = function(...args) {
|
|
143
167
|
handleConsoleCall(key, args);
|
|
144
168
|
|
|
145
169
|
// Calls and returns the original console function.
|
|
146
170
|
return original.apply(this, arguments);
|
|
147
171
|
};
|
|
172
|
+
console[key] = wrapper;
|
|
173
|
+
consoleWrappers.push({key, original, wrapper});
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
/**
|
|
@@ -251,23 +277,41 @@
|
|
|
251
277
|
if (value !== undefined) addLog('result', [value], {}, 'string' === typeof value ? 1 : 0);
|
|
252
278
|
}
|
|
253
279
|
|
|
254
|
-
|
|
280
|
+
function onError(evt) {
|
|
255
281
|
reportUncaught(evt.error !== undefined ? evt.error : evt.message);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
addEventListener('unhandledrejection', evt => {
|
|
282
|
+
}
|
|
283
|
+
function onUnhandledRejection(evt) {
|
|
259
284
|
reportUncaught(evt.reason, true);
|
|
260
|
-
}
|
|
285
|
+
}
|
|
286
|
+
addEventListener('error', onError);
|
|
287
|
+
addEventListener('unhandledrejection', onUnhandledRejection);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Stops capturing what is logged (used when a console in window mode is
|
|
291
|
+
* destroyed). Each console function is restored unless something else has
|
|
292
|
+
* replaced it since.
|
|
293
|
+
*/
|
|
294
|
+
function destroy() {
|
|
295
|
+
removeEventListener('error', onError);
|
|
296
|
+
removeEventListener('unhandledrejection', onUnhandledRejection);
|
|
297
|
+
delete globalThis[Symbol.for(RESULT_KEY)];
|
|
298
|
+
for (const {key, original, wrapper} of consoleWrappers) {
|
|
299
|
+
if (console[key] === wrapper) console[key] = original;
|
|
300
|
+
}
|
|
301
|
+
clearLogs();
|
|
302
|
+
}
|
|
261
303
|
|
|
262
304
|
/**
|
|
263
305
|
* @param {*} error
|
|
264
306
|
* @param {boolean=} isInPromise
|
|
265
307
|
*/
|
|
266
308
|
function reportUncaught(error, isInPromise) {
|
|
267
|
-
// Points out how to use top-level await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
: ''
|
|
309
|
+
// Points out how to use top-level await or import statements if they were
|
|
310
|
+
// used in a classic block.
|
|
311
|
+
const feature = runningBlockType === 'classic' && error?.name === 'SyntaxError'
|
|
312
|
+
? /\bawait\b/.test(error.message) ? 'top-level await' : /\bimport\b/.test(error.message) ? 'import statements' : null
|
|
313
|
+
: null;
|
|
314
|
+
const hint = feature ? `\n(To use ${feature}, add data-block-type="module" to the script tag.)` : '';
|
|
271
315
|
send({
|
|
272
316
|
target: 'viewer',
|
|
273
317
|
func: 'appendError',
|
|
@@ -780,7 +824,7 @@
|
|
|
780
824
|
groupIds = [];
|
|
781
825
|
}
|
|
782
826
|
|
|
783
|
-
return {clearLogs, runCode, sendDescriptionFor};
|
|
827
|
+
return {clearLogs, destroy, runCode, sendDescriptionFor};
|
|
784
828
|
}
|
|
785
829
|
|
|
786
830
|
|
|
@@ -850,6 +894,10 @@
|
|
|
850
894
|
start();
|
|
851
895
|
|
|
852
896
|
return {
|
|
897
|
+
destroy() {
|
|
898
|
+
worker.terminate();
|
|
899
|
+
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
900
|
+
},
|
|
853
901
|
apply(func, args) {
|
|
854
902
|
if (func === 'reset') {
|
|
855
903
|
worker.terminate();
|
|
@@ -873,6 +921,9 @@
|
|
|
873
921
|
function createWindowRunner(onMessage) {
|
|
874
922
|
const runner = createRunner(onMessage, {mode: 'window', ownUrl: OWN_URL});
|
|
875
923
|
return {
|
|
924
|
+
destroy() {
|
|
925
|
+
runner.destroy();
|
|
926
|
+
},
|
|
876
927
|
apply(func, args) {
|
|
877
928
|
// Anything the code defined stays defined so a reset can only forget
|
|
878
929
|
// the logged values.
|
|
@@ -883,16 +934,27 @@
|
|
|
883
934
|
}
|
|
884
935
|
|
|
885
936
|
/**
|
|
886
|
-
*
|
|
887
|
-
* @param {
|
|
888
|
-
*
|
|
889
|
-
*
|
|
937
|
+
* Creates a console.
|
|
938
|
+
* @param {Object} options
|
|
939
|
+
* @param {string} options.code
|
|
940
|
+
* The code that the console starts with.
|
|
941
|
+
* @param {{[name: string]: string}} options.dataset
|
|
942
|
+
* The options for the console in the same form as the data attributes of
|
|
943
|
+
* a script tag (eg. `{runner: 'window', hidePrefix: 'HIDE'}`).
|
|
944
|
+
* @param {string=} options.height
|
|
945
|
+
* Optional, defaults to `"100%"`. The CSS height of the console.
|
|
946
|
+
* @param {(element: HTMLIFrameElement) => void} options.insert
|
|
947
|
+
* Puts the console's element into the page.
|
|
948
|
+
* @returns {{element: HTMLIFrameElement, destroy: () => void}}
|
|
890
949
|
*/
|
|
891
|
-
function
|
|
892
|
-
const dataset = JSON.parse(JSON.stringify(script.dataset));
|
|
950
|
+
function createConsole({code, dataset, height, insert}) {
|
|
893
951
|
const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
|
|
894
952
|
const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
|
|
895
953
|
const showResults = dataset.showResults !== 'false';
|
|
954
|
+
// An empty data-imports-url turns off loading packages by name.
|
|
955
|
+
const importsUrl = dataset.importsUrl == null
|
|
956
|
+
? DEFAULT_IMPORTS_URL
|
|
957
|
+
: dataset.importsUrl && resolveUrlTemplate(dataset.importsUrl);
|
|
896
958
|
const libraryUrl = getLibraryFileUrl.bind(null, dataset.librariesUrl || DEFAULT_LIBRARIES_URL);
|
|
897
959
|
|
|
898
960
|
// The viewer in the page and, when the console is popped out, the viewer
|
|
@@ -993,7 +1055,8 @@
|
|
|
993
1055
|
};
|
|
994
1056
|
|
|
995
1057
|
// The pop-out window can't work without this page so it is closed too.
|
|
996
|
-
|
|
1058
|
+
const closePopOut = () => popOutWindow?.close();
|
|
1059
|
+
addEventListener('pagehide', closePopOut);
|
|
997
1060
|
|
|
998
1061
|
/**
|
|
999
1062
|
* @param {Window=} targetWindow
|
|
@@ -1081,8 +1144,8 @@
|
|
|
1081
1144
|
|
|
1082
1145
|
const hidePrefix = dataset.hidePrefix ?? '';
|
|
1083
1146
|
const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
|
|
1084
|
-
const
|
|
1085
|
-
const {visibleCode, hiddenGroups} = extractHiddenGroups(
|
|
1147
|
+
const startingCode = unindentMin(jsCode);
|
|
1148
|
+
const {visibleCode, hiddenGroups} = extractHiddenGroups(startingCode, hidePrefix);
|
|
1086
1149
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
|
|
1087
1150
|
const copyHiddenGroups = () => hiddenGroups.map(group => ({...group}));
|
|
1088
1151
|
|
|
@@ -1094,6 +1157,12 @@
|
|
|
1094
1157
|
hidePrefix,
|
|
1095
1158
|
hiddenGroups: copyHiddenGroups(),
|
|
1096
1159
|
runnerMode: meta.runnerMode,
|
|
1160
|
+
importsUrl: meta.importsUrl,
|
|
1161
|
+
// The code the console starts with (and goes back to when reset)
|
|
1162
|
+
// which changes when a file is opened.
|
|
1163
|
+
originalCode: startingCode,
|
|
1164
|
+
// The name of the file that was last opened or saved.
|
|
1165
|
+
fileName: null,
|
|
1097
1166
|
blockType: meta.blockType,
|
|
1098
1167
|
showResults: meta.showResults,
|
|
1099
1168
|
packageInfo: meta.packageInfo,
|
|
@@ -1197,12 +1266,23 @@
|
|
|
1197
1266
|
/**
|
|
1198
1267
|
* The code that the copied console will start with.
|
|
1199
1268
|
*/
|
|
1269
|
+
/**
|
|
1270
|
+
* The original code split into the code for the editor and the
|
|
1271
|
+
* hidden groups.
|
|
1272
|
+
*/
|
|
1273
|
+
originalGroups() {
|
|
1274
|
+
return extractHiddenGroups(this.originalCode, this.hidePrefix);
|
|
1275
|
+
},
|
|
1200
1276
|
exportJsCode() {
|
|
1201
|
-
if (this.exportCode === 'original') return originalCode;
|
|
1277
|
+
if (this.exportCode === 'original') return this.originalCode;
|
|
1202
1278
|
if (this.exportCode === 'blank') return '';
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1279
|
+
return this.currentCode;
|
|
1280
|
+
},
|
|
1281
|
+
/**
|
|
1282
|
+
* The groups that already ran, then the groups in the editor with any
|
|
1283
|
+
* hidden groups that haven't run yet in their original places.
|
|
1284
|
+
*/
|
|
1285
|
+
currentCode() {
|
|
1206
1286
|
const blocks = this.runHistory.slice();
|
|
1207
1287
|
const pendingHiddenGroups = this.hiddenGroups.slice();
|
|
1208
1288
|
let visibleCount = this.runCount;
|
|
@@ -1349,17 +1429,17 @@
|
|
|
1349
1429
|
});
|
|
1350
1430
|
this.runningCount++;
|
|
1351
1431
|
this.runHistory.push(group.allLines);
|
|
1432
|
+
const {code, resultRange} = prepareCode(group.lines, {
|
|
1433
|
+
blockType: this.blockType,
|
|
1434
|
+
importsUrl: this.importsUrl,
|
|
1435
|
+
// Like the browser's console, show the value of the last
|
|
1436
|
+
// expression (except for hidden code).
|
|
1437
|
+
findResult: this.showResults && !isHidden,
|
|
1438
|
+
});
|
|
1352
1439
|
messageParent({
|
|
1353
1440
|
target: 'runner',
|
|
1354
1441
|
func: 'runCode',
|
|
1355
|
-
args: [
|
|
1356
|
-
blockType: this.blockType,
|
|
1357
|
-
// Like the browser's console, show the value of the last
|
|
1358
|
-
// expression (except for hidden code).
|
|
1359
|
-
resultRange: this.showResults && !isHidden
|
|
1360
|
-
? findLastExpression(group.lines, this.blockType)
|
|
1361
|
-
: null,
|
|
1362
|
-
}],
|
|
1442
|
+
args: [code, {blockType: this.blockType, resultRange}],
|
|
1363
1443
|
});
|
|
1364
1444
|
},
|
|
1365
1445
|
clearConsole() {
|
|
@@ -1376,16 +1456,87 @@
|
|
|
1376
1456
|
confirmText: 'Reset',
|
|
1377
1457
|
});
|
|
1378
1458
|
if (!isConfirmed) return;
|
|
1379
|
-
|
|
1459
|
+
this.restart();
|
|
1460
|
+
},
|
|
1461
|
+
/**
|
|
1462
|
+
* Starts over with the original code (clearing the output and, in
|
|
1463
|
+
* worker mode, starting a new worker).
|
|
1464
|
+
*/
|
|
1465
|
+
restart() {
|
|
1466
|
+
const {visibleCode, hiddenGroups} = this.originalGroups;
|
|
1380
1467
|
this.displays = [];
|
|
1381
1468
|
this.jsCode = visibleCode;
|
|
1382
|
-
this.hiddenGroups =
|
|
1469
|
+
this.hiddenGroups = hiddenGroups.map(group => ({...group}));
|
|
1383
1470
|
this.runCount = 0;
|
|
1384
1471
|
this.runningCount = 0;
|
|
1385
1472
|
this.runHistory = [];
|
|
1386
1473
|
messageParent({target: 'runner', func: 'reset', args: []});
|
|
1387
1474
|
this.runHiddenGroups();
|
|
1388
1475
|
},
|
|
1476
|
+
/**
|
|
1477
|
+
* Shows the file picker for opening a JavaScript file.
|
|
1478
|
+
*/
|
|
1479
|
+
openFile() {
|
|
1480
|
+
this.closeMenu();
|
|
1481
|
+
const {fileInput} = this.$refs;
|
|
1482
|
+
fileInput.value = '';
|
|
1483
|
+
fileInput.click();
|
|
1484
|
+
},
|
|
1485
|
+
/**
|
|
1486
|
+
* Makes the chosen file's code the code that the console starts with.
|
|
1487
|
+
* @param {Event} evt
|
|
1488
|
+
*/
|
|
1489
|
+
async onFileChosen(evt) {
|
|
1490
|
+
const file = evt.target.files?.[0];
|
|
1491
|
+
if (!file) return;
|
|
1492
|
+
const code = unindentMin(await file.text());
|
|
1493
|
+
if (this.displays.length || this.jsCode.trim()) {
|
|
1494
|
+
const isConfirmed = await this.confirm({
|
|
1495
|
+
title: `Open ${file.name}?`,
|
|
1496
|
+
message: 'The output will be cleared and the code in the editor will be replaced with the code in the file.'
|
|
1497
|
+
+ (this.runnerMode === 'window'
|
|
1498
|
+
? ' Anything the code already defined on the page will stay defined.'
|
|
1499
|
+
: ''),
|
|
1500
|
+
confirmText: 'Open',
|
|
1501
|
+
});
|
|
1502
|
+
if (!isConfirmed) return;
|
|
1503
|
+
}
|
|
1504
|
+
this.fileName = file.name;
|
|
1505
|
+
this.originalCode = code;
|
|
1506
|
+
this.restart();
|
|
1507
|
+
},
|
|
1508
|
+
/**
|
|
1509
|
+
* Saves the current code (what already ran and what is in the editor)
|
|
1510
|
+
* as a JavaScript file which can be opened later.
|
|
1511
|
+
*/
|
|
1512
|
+
async saveFile() {
|
|
1513
|
+
this.closeMenu();
|
|
1514
|
+
const code = this.currentCode + '\n';
|
|
1515
|
+
const suggestedName = this.fileName || 'js-box.js';
|
|
1516
|
+
|
|
1517
|
+
// Shows a "Save as" dialog where it is supported.
|
|
1518
|
+
if ('function' === typeof window.showSaveFilePicker) {
|
|
1519
|
+
try {
|
|
1520
|
+
const handle = await showSaveFilePicker({
|
|
1521
|
+
suggestedName,
|
|
1522
|
+
types: [{description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}}],
|
|
1523
|
+
});
|
|
1524
|
+
const writable = await handle.createWritable();
|
|
1525
|
+
await writable.write(code);
|
|
1526
|
+
await writable.close();
|
|
1527
|
+
this.fileName = handle.name;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
catch (e) {
|
|
1531
|
+
// The user cancelled. For other errors the file is downloaded.
|
|
1532
|
+
if (e?.name === 'AbortError') return;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const url = URL.createObjectURL(new Blob([code], {type: 'text/javascript'}));
|
|
1537
|
+
Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
|
|
1538
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1539
|
+
},
|
|
1389
1540
|
/**
|
|
1390
1541
|
* Copies code that was already run into the editor so that it can be run
|
|
1391
1542
|
* again (as is or modified), first asking before replacing any code that
|
|
@@ -1484,6 +1635,8 @@
|
|
|
1484
1635
|
return JSON.parse(JSON.stringify({
|
|
1485
1636
|
displays: this.displays,
|
|
1486
1637
|
jsCode: this.jsCode,
|
|
1638
|
+
originalCode: this.originalCode,
|
|
1639
|
+
fileName: this.fileName,
|
|
1487
1640
|
runHistory: this.runHistory,
|
|
1488
1641
|
hiddenGroups: this.hiddenGroups,
|
|
1489
1642
|
runCount: this.runCount,
|
|
@@ -1956,6 +2109,8 @@
|
|
|
1956
2109
|
// https://icon-sets.iconify.design/mdi/error-outline/
|
|
1957
2110
|
error: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8"/></svg>',
|
|
1958
2111
|
// Icons similar to those in the browser's console.
|
|
2112
|
+
open: '<svg viewBox="0 0 16 16"><path d="M2 4.5a1 1 0 0 1 1-1h3.2l1.3 1.5H13a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>',
|
|
2113
|
+
save: '<svg viewBox="0 0 16 16"><path d="M8 2.5v7M5 7l3 3 3-3M3 11.5v1.5h10v-1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1959
2114
|
fullscreen: '<svg viewBox="0 0 16 16"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1960
2115
|
exitFullscreen: '<svg viewBox="0 0 16 16"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
1961
2116
|
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>',
|
|
@@ -2148,30 +2303,97 @@
|
|
|
2148
2303
|
}
|
|
2149
2304
|
|
|
2150
2305
|
/**
|
|
2151
|
-
*
|
|
2152
|
-
*
|
|
2306
|
+
* Gets a block of code ready to run: packages imported by name (eg.
|
|
2307
|
+
* `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
|
|
2308
|
+
* and the last statement is found if it is an expression so that its value
|
|
2309
|
+
* can be shown.
|
|
2153
2310
|
* @param {string} code
|
|
2154
|
-
* @param {
|
|
2155
|
-
* @
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2311
|
+
* @param {Object} options
|
|
2312
|
+
* @param {"classic"|"module"} options.blockType
|
|
2313
|
+
* @param {string?} options.importsUrl
|
|
2314
|
+
* The URL template where `{specifier}` is replaced with what was imported
|
|
2315
|
+
* or an empty value to leave imports alone.
|
|
2316
|
+
* @param {boolean} options.findResult
|
|
2317
|
+
* @returns {{code: string, resultRange: [number, number]|null}}
|
|
2318
|
+
* The code to run and the start and end index of its last expression (or
|
|
2319
|
+
* `null`). If the code can't be parsed it is returned as is so that running
|
|
2320
|
+
* it shows the syntax error.
|
|
2159
2321
|
*/
|
|
2160
|
-
function
|
|
2161
|
-
if (!window.acorn) return null;
|
|
2322
|
+
function prepareCode(code, {blockType, importsUrl, findResult}) {
|
|
2323
|
+
if (!window.acorn) return {code, resultRange: null};
|
|
2324
|
+
let ast;
|
|
2162
2325
|
try {
|
|
2163
|
-
|
|
2326
|
+
ast = acorn.parse(code, {
|
|
2164
2327
|
ecmaVersion: 'latest',
|
|
2165
2328
|
sourceType: blockType === 'module' ? 'module' : 'script',
|
|
2166
2329
|
allowHashBang: true,
|
|
2167
2330
|
});
|
|
2168
|
-
|
|
2331
|
+
}
|
|
2332
|
+
catch (e) {
|
|
2333
|
+
return {code, resultRange: null};
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
let resultRange = null;
|
|
2337
|
+
if (findResult) {
|
|
2338
|
+
const last = ast.body[ast.body.length - 1];
|
|
2169
2339
|
if (last?.type === 'ExpressionStatement' && !last.directive) {
|
|
2170
|
-
|
|
2340
|
+
resultRange = [last.expression.start, last.expression.end];
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// Finds every import (and re-export) of a package by name.
|
|
2345
|
+
const replacements = [];
|
|
2346
|
+
if (importsUrl) {
|
|
2347
|
+
walkAst(ast, node => {
|
|
2348
|
+
const {source} = /^(Import(Declaration|Expression)|Export(All|Named)Declaration)$/.test(node.type) ? node : {};
|
|
2349
|
+
if (source?.type === 'Literal' && 'string' === typeof source.value && isBareSpecifier(source.value)) {
|
|
2350
|
+
replacements.push({
|
|
2351
|
+
start: source.start,
|
|
2352
|
+
end: source.end,
|
|
2353
|
+
text: JSON.stringify(importsUrl.replace(/\{specifier\}/g, source.value)),
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Replaces them from last to first, moving the result's range as needed.
|
|
2360
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
2361
|
+
for (const {start, end, text} of replacements) {
|
|
2362
|
+
code = code.slice(0, start) + text + code.slice(end);
|
|
2363
|
+
const shift = text.length - (end - start);
|
|
2364
|
+
if (resultRange) resultRange = resultRange.map(index => index >= end ? index + shift : index);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
return {code, resultRange};
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Indicates if an import specifier is a package name (eg. "lodash",
|
|
2372
|
+
* "lodash@4/fp" or "@scope/pkg") rather than a relative path or a URL (eg.
|
|
2373
|
+
* "./utils.js", "https://example.com/x.js" or "node:fs").
|
|
2374
|
+
* @param {string} specifier
|
|
2375
|
+
* @returns {boolean}
|
|
2376
|
+
*/
|
|
2377
|
+
function isBareSpecifier(specifier) {
|
|
2378
|
+
return !/^(\.{0,2}\/|#|[a-z][a-z\d+.-]*:)/i.test(specifier);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/**
|
|
2382
|
+
* Calls the callback for every node in an Acorn syntax tree.
|
|
2383
|
+
* @param {*} node
|
|
2384
|
+
* @param {(node: *) => void} callback
|
|
2385
|
+
*/
|
|
2386
|
+
function walkAst(node, callback) {
|
|
2387
|
+
if (Array.isArray(node)) {
|
|
2388
|
+
for (const child of node) walkAst(child, callback);
|
|
2389
|
+
}
|
|
2390
|
+
else if (node && 'string' === typeof node.type) {
|
|
2391
|
+
callback(node);
|
|
2392
|
+
for (const key of Object.keys(node)) {
|
|
2393
|
+
const value = node[key];
|
|
2394
|
+
if (value && 'object' === typeof value) walkAst(value, callback);
|
|
2171
2395
|
}
|
|
2172
2396
|
}
|
|
2173
|
-
catch (e) {}
|
|
2174
|
-
return null;
|
|
2175
2397
|
}
|
|
2176
2398
|
|
|
2177
2399
|
/**
|
|
@@ -2460,9 +2682,9 @@
|
|
|
2460
2682
|
libraryUrl('prismjs', 'components/prism-core.min.js'),
|
|
2461
2683
|
libraryUrl('prismjs', 'plugins/autoloader/prism-autoloader.min.js'),
|
|
2462
2684
|
libraryUrl('prismjs', 'plugins/match-braces/prism-match-braces.min.js'),
|
|
2463
|
-
// Used to find the last expression in each block of code so that its
|
|
2464
|
-
// value can be shown.
|
|
2465
|
-
|
|
2685
|
+
// Used to find the last expression in each block of code (so that its
|
|
2686
|
+
// value can be shown) and the packages that it imports.
|
|
2687
|
+
libraryUrl('acorn', 'dist/acorn.js'),
|
|
2466
2688
|
],
|
|
2467
2689
|
cssUrls: [
|
|
2468
2690
|
'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
|
|
@@ -2478,10 +2700,11 @@
|
|
|
2478
2700
|
async onReady() {
|
|
2479
2701
|
// The dataset is passed as is so that the viewer knows which options
|
|
2480
2702
|
// were actually specified (eg. when copying the console as HTML).
|
|
2481
|
-
this.call('init',
|
|
2703
|
+
this.call('init', code, dataset, {
|
|
2482
2704
|
runnerMode,
|
|
2483
2705
|
blockType,
|
|
2484
2706
|
showResults,
|
|
2707
|
+
importsUrl,
|
|
2485
2708
|
packageInfo: PACKAGE_INFO,
|
|
2486
2709
|
libraryVersions: LIBRARY_VERSIONS,
|
|
2487
2710
|
isPopOut: !!targetWindow,
|
|
@@ -2491,7 +2714,10 @@
|
|
|
2491
2714
|
body: VIEWER_IFRAME_HTML,
|
|
2492
2715
|
style: {
|
|
2493
2716
|
width: '100%',
|
|
2494
|
-
height
|
|
2717
|
+
// Fills its container unless a height is given but is never
|
|
2718
|
+
// squashed smaller than the default height of an IFRAME.
|
|
2719
|
+
height: height || '100%',
|
|
2720
|
+
minHeight: '150px',
|
|
2495
2721
|
border: 0,
|
|
2496
2722
|
display: 'block',
|
|
2497
2723
|
},
|
|
@@ -2499,9 +2725,124 @@
|
|
|
2499
2725
|
}
|
|
2500
2726
|
|
|
2501
2727
|
inlineViewer = activeViewer = createViewer();
|
|
2502
|
-
|
|
2728
|
+
insert(inlineViewer.iframe);
|
|
2729
|
+
|
|
2730
|
+
let isDestroyed = false;
|
|
2731
|
+
return {
|
|
2732
|
+
element: inlineViewer.iframe,
|
|
2733
|
+
/**
|
|
2734
|
+
* Removes the console, stops its code and closes its pop-out window.
|
|
2735
|
+
*/
|
|
2736
|
+
destroy() {
|
|
2737
|
+
if (isDestroyed) return;
|
|
2738
|
+
isDestroyed = true;
|
|
2739
|
+
if (popOutWindow) {
|
|
2740
|
+
clearInterval(popOutWatcher);
|
|
2741
|
+
popOutViewer.dispose();
|
|
2742
|
+
popOutWindow.close();
|
|
2743
|
+
popOutWindow = null;
|
|
2744
|
+
}
|
|
2745
|
+
removeEventListener('pagehide', closePopOut);
|
|
2746
|
+
runner.destroy();
|
|
2747
|
+
inlineViewer.dispose();
|
|
2748
|
+
inlineViewer.iframe.remove();
|
|
2749
|
+
},
|
|
2750
|
+
};
|
|
2503
2751
|
}
|
|
2504
2752
|
|
|
2753
|
+
/**
|
|
2754
|
+
* Creates a console in place of a script tag using its code and data
|
|
2755
|
+
* attributes.
|
|
2756
|
+
* @param {HTMLScriptElement} script
|
|
2757
|
+
*/
|
|
2758
|
+
function createConsoleFromScript(script) {
|
|
2759
|
+
return createConsole({
|
|
2760
|
+
code: script.textContent,
|
|
2761
|
+
dataset: JSON.parse(JSON.stringify(script.dataset)),
|
|
2762
|
+
insert: element => script.parentNode.insertBefore(element, script),
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* Where YourJSBox.create() can put a console relative to its target.
|
|
2768
|
+
*/
|
|
2769
|
+
const PLACEMENTS = {
|
|
2770
|
+
fill: (target, element) => target.replaceChildren(element),
|
|
2771
|
+
append: (target, element) => target.append(element),
|
|
2772
|
+
prepend: (target, element) => target.prepend(element),
|
|
2773
|
+
replace: (target, element) => target.replaceWith(element),
|
|
2774
|
+
before: (target, element) => target.before(element),
|
|
2775
|
+
after: (target, element) => target.after(element),
|
|
2776
|
+
};
|
|
2777
|
+
|
|
2778
|
+
/**
|
|
2779
|
+
* The options of YourJSBox.create() which are the same as the data
|
|
2780
|
+
* attributes of a script tag.
|
|
2781
|
+
*/
|
|
2782
|
+
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
2783
|
+
|
|
2784
|
+
/**
|
|
2785
|
+
* The JavaScript API for creating consoles (available as window.YourJSBox).
|
|
2786
|
+
*/
|
|
2787
|
+
const YourJSBox = Object.freeze({
|
|
2788
|
+
version: PACKAGE_INFO.version,
|
|
2789
|
+
|
|
2790
|
+
/**
|
|
2791
|
+
* Creates a console.
|
|
2792
|
+
* @param {Object} options
|
|
2793
|
+
* @param {string|Element} options.target
|
|
2794
|
+
* The element (or a CSS selector for it) that the console is placed
|
|
2795
|
+
* relative to.
|
|
2796
|
+
* @param {"fill"|"append"|"prepend"|"replace"|"before"|"after"=} options.placement
|
|
2797
|
+
* Optional, defaults to `"fill"`. Where the console goes: "fill"
|
|
2798
|
+
* replaces the target's contents, "append" and "prepend" add it inside
|
|
2799
|
+
* of the target, "replace" replaces the target itself and "before" and
|
|
2800
|
+
* "after" add it next to the target.
|
|
2801
|
+
* @param {(string|number)=} options.height
|
|
2802
|
+
* Optional, defaults to `"100%"`. The CSS height of the console (a
|
|
2803
|
+
* number is treated as pixels). It is never less than 150px.
|
|
2804
|
+
* @param {string=} options.code
|
|
2805
|
+
* Optional. The code that the console starts with.
|
|
2806
|
+
* @param {string=} options.runner
|
|
2807
|
+
* @param {string=} options.blockType
|
|
2808
|
+
* @param {(boolean|string)=} options.showResults
|
|
2809
|
+
* @param {string=} options.hidePrefix
|
|
2810
|
+
* @param {string=} options.dividerOrient
|
|
2811
|
+
* @param {string=} options.theme
|
|
2812
|
+
* @param {string=} options.librariesUrl
|
|
2813
|
+
* @param {string=} options.importsUrl
|
|
2814
|
+
* The same as the data attributes of a script tag.
|
|
2815
|
+
* @returns {{element: HTMLIFrameElement, destroy: () => void}}
|
|
2816
|
+
*/
|
|
2817
|
+
create(options) {
|
|
2818
|
+
options = Object(options);
|
|
2819
|
+
const {target, placement = 'fill', height, code = ''} = options;
|
|
2820
|
+
const targetElement = 'string' === typeof target ? document.querySelector(target) : target;
|
|
2821
|
+
if (targetElement?.nodeType !== 1) {
|
|
2822
|
+
throw new TypeError(
|
|
2823
|
+
'string' === typeof target
|
|
2824
|
+
? `YourJSBox.create(): no element matches the target ${JSON.stringify(target)}.`
|
|
2825
|
+
: 'YourJSBox.create(): target must be an element or a CSS selector.'
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (!Object.hasOwn(PLACEMENTS, placement)) {
|
|
2829
|
+
throw new TypeError(`YourJSBox.create(): placement must be one of ${Object.keys(PLACEMENTS).join(', ')}.`);
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
const dataset = {};
|
|
2833
|
+
for (const name of CONSOLE_OPTION_NAMES) {
|
|
2834
|
+
if (options[name] != null) dataset[name] = `${options[name]}`;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
return createConsole({
|
|
2838
|
+
code: `${code}`,
|
|
2839
|
+
dataset,
|
|
2840
|
+
height: 'number' === typeof height ? `${height}px` : height != null ? `${height}` : undefined,
|
|
2841
|
+
insert: element => PLACEMENTS[placement](targetElement, element),
|
|
2842
|
+
});
|
|
2843
|
+
},
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2505
2846
|
// NOTE: This solution was intentionally written without using newer JS
|
|
2506
2847
|
// features to make the minified version even smaller.
|
|
2507
2848
|
var createCallableFrame = (function () {
|
|
@@ -2789,5 +3130,13 @@
|
|
|
2789
3130
|
return createCallableFrame;
|
|
2790
3131
|
})();
|
|
2791
3132
|
|
|
2792
|
-
|
|
3133
|
+
// The first copy of this script that is loaded provides the API.
|
|
3134
|
+
if (!window.YourJSBox) window.YourJSBox = YourJSBox;
|
|
3135
|
+
|
|
3136
|
+
// A script in the body is replaced by a console while a script in the head
|
|
3137
|
+
// only provides the API.
|
|
3138
|
+
const currentScript = document.currentScript;
|
|
3139
|
+
if (currentScript && !document.head?.contains(currentScript)) {
|
|
3140
|
+
createConsoleFromScript(currentScript);
|
|
3141
|
+
}
|
|
2793
3142
|
})();
|