UNPKG

mockm

Version:

Analog interface server, painless parallel development of front and back ends.

1,085 lines (1,060 loc) 38.9 kB
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>restc</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./../cdn/gh/highlightjs/cdn-release@9.8.0/build/styles/default.min.css"> <link rel="stylesheet" href="./../cdn/gh/codemirror/CodeMirror@5.19.0/lib/codemirror.css"></script> <link rel="stylesheet" href="./../cdn/gh/codemirror/CodeMirror@5.19.0/theme/material.css"></script> <script> if (!window.fetch) document.write('<script src="./../cdn/gh/github/fetch@v1.0.0/fetch.min.js"><\/script>'); </script> <script src="./../cdn/npm/jinkela@1.2.18/umd.min.js"></script> <script src="./../cdn/gh/highlightjs/cdn-release@9.8.0/build/highlight.min.js"></script> <script src="./../cdn/gh/codemirror/CodeMirror@5.19.0/lib/codemirror.min.js"></script> <script src="./../cdn/gh/codemirror/CodeMirror@5.19.0/mode/javascript/javascript.min.js"></script> <script src="./../cdn/gh/codemirror/CodeMirror@5.19.0/addon/display/autorefresh.min.js"></script> <script src="./../cdn/npm/json-format-safely@1.2.2/index.js"></script> <script extract> class Configuration { static get watchList() { let value = {}; Object.defineProperty(this, 'watchList', { value, configurable: true }); return value; } static set(key, value) { let oldValue = this.get(key); localStorage.setItem(key, JSON.stringify(value)); if (this.watchList.hasOwnProperty(key)) { this.watchList[key].forEach(watcher => watcher.call(this, value, oldValue)); } } static get(key, defaultValue = null) { try { let value = JSON.parse(localStorage.getItem(key)); if (value === null) value = defaultValue; return value; } catch (error) { return defaultValue; } } static watch(key, callback, callImmediately = true) { (this.watchList.hasOwnProperty(key) ? this.watchList[key] : this.watchList[key] = []).push(callback); if (callImmediately) this.set(key, this.get(key)); } static unwatch(key, callback) { if (this.watchList.hasOwnProperty(key)) { this.watchList[key] = this.watchList[key].filter(watcher => watcher !== callback); } } } class QueryString { static encode(s) { return encodeURIComponent(s).replace(/%20/g, '+'); } static decode(s) { return decodeURIComponent(s.replace(/\+/g, '%20')); } static parse(queryString, preserveOrder = false) { let result = queryString.split('&') .filter(entry => entry.length) .map(entry => entry.split('=')) .map(([ key, value = '' ]) => ({ key: this.decode(key), value: this.decode(value) })); if (preserveOrder) return result; return result.reduce((result, { key, value }) => Object.assign(result, { [key]: value }), {}); } static stringify(parameters) { if (!Array.isArray(parameters)) { parameters = Object.keys(parameters).map(key => ({ key, value: parameters[key] })); } return parameters .map(({ key, value }) => [ this.encode(key), this.encode(value) ].join('=')) .join('&'); } } const isBodyEnabled = method => ![ 'HEAD', 'GET' ].includes(method); const loader = ({ element }, $promise) => { element.classList.add('loading'); let restore = () => { element.classList.remove('loading'); }; return $promise.then(data => { restore(); return data; }, e => { restore(); throw e; }); } class HighlightBlock extends Jinkela { get template() { return `<pre><code class="http" ref="body"></code></pre>`; } update(code, ...elements) { this.body.textContent = code; hljs.highlightBlock(this.body); for (let element of elements) { if (element instanceof Jinkela) element.to(this.body); } } get styleSheet() { return ` :scope { font-size: 12px; line-height: 1.5; & > code { margin: 0 8px; padding: 20px 32px; background: transparent; overflow: visible; } } `; } } class RequestView extends Jinkela { constructor(...args) { super(...args); } init() { this.block = new HighlightBlock().to(this); } update({ method, path, queryString, body, headers }) { let uri = path; if (queryString) uri += '?' + queryString; headers = Object.keys(headers) .map(key => ({ key, value: headers[key] })) .map(({ key, value }) => ({ key, values: Array.isArray(value) ? value : [ value ] })) .reduce((result, { key, values }) => [ ...result, ...values.map(value => ({ key, value })) ], []); let request = [ [ method, uri, 'HTTP/1.1' ].join(' '), ...headers.map(({ key, value }) => [ key, value ].join(': ')) ].join('\n'); if (isBodyEnabled(method) && body) request = [ request, body ].join('\n\n'); this.block.update(request); } } class ResponseViewImage extends Jinkela { beforeParse(params) { if (!params.filename) { params.filename = ''; } } get template() { return ` <a download="{filename}" href="{url}"> <img src="{url}"/> </a> `; } get styleSheet() { return ` :scope { text-decoration: none; } `; } } class ResponseViewDownloadLink extends Jinkela { beforeParse(params) { if (!params.filename) { params.filename = ''; } } get template() { return ` <a download="{filename}" href="{url}"> <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1022.955 522.57c0 100.193-81.516 181.699-181.719 181.699H655.6c-11.298 0-20.467-9.169-20.467-20.466 0-11.308 9.17-20.466 20.467-20.466h185.637c77.628 0 140.787-63.148 140.787-140.766 0-77.424-62.841-140.449-140.203-140.766-.42.03-.819.05-1.218.061-5.945.143-11.686-2.292-15.687-6.703a20.455 20.455 0 0 1-5.168-16.25c1.33-10.806 1.944-19.76 1.944-28.192 0-60.764-23.658-117.885-66.617-160.833-42.969-42.968-100.09-66.617-160.843-66.617-47.369 0-92.742 14.45-131.208 41.782-37.617 26.739-65.953 63.7-81.926 106.884a20.5 20.5 0 0 1-14.828 12.894 20.492 20.492 0 0 1-18.86-5.547c-19.289-19.33-44.943-29.972-72.245-29.972-56.323 0-102.146 45.813-102.146 102.126 0 .317.04.982.092 1.627.061.92.122 1.831.153 2.763a20.466 20.466 0 0 1-15.002 20.455c-32.356 8.934-61.541 28.55-82.181 55.218-21.305 27.517-32.572 60.508-32.572 95.413 0 86.244 70.188 156.423 156.443 156.423h169.981c11.298 0 20.466 9.158 20.466 20.466 0 11.297-9.168 20.466-20.466 20.466H199.951c-108.829 0-197.375-88.536-197.375-197.355 0-44.053 14.224-85.712 41.126-120.474 22.81-29.46 53.898-52.086 88.71-64.816 5.066-74.323 67.15-133.245 142.752-133.245 28.386 0 55.504 8.218 78.651 23.526 19.658-39.868 48.843-74.169 85.498-100.212 45.434-32.296 99.004-49.354 154.918-49.354 71.693 0 139.088 27.916 189.782 78.6 50.695 50.695 78.61 118.09 78.61 189.782 0 3.705-.102 7.47-.296 11.37 90.307 10.478 160.628 87.42 160.628 180.48z"/><path d="M629.259 820.711L527.235 922.724c-3.99 4.002-9.23 5.997-14.47 5.997s-10.478-1.995-14.47-5.997L396.273 820.711c-7.992-7.992-7.992-20.947 0-28.94s20.947-8.001 28.94 0l67.087 67.079v-358.7c0-11.297 9.159-20.466 20.466-20.466 11.308 0 20.467 9.169 20.467 20.466v358.7l67.088-67.078c7.992-8.002 20.947-7.992 28.939 0s7.992 20.947 0 28.939z"/></svg> Download <span>{filename}</span> </a> `; } get styleSheet() { return ` :scope { --r: 76; --g: 82; --b: 100; --a: .9; background: rgba(var(--r), var(--g), var(--b), var(--a)); border-radius: 4px; color: #fff; display: inline-block; fill: #fff; font-family: sans-serif; padding: 10px 15px; text-decoration: none; transition: background .2s ease; white-space: nowrap; > svg { display: inline; vertical-align: middle; } &:hover { --a: .8; } &:active { --a: 1; } } `; } } class ResponseView extends Jinkela { constructor(...args) { super(...args); } init() { this.block = new HighlightBlock().to(this); } update($response) { return Promise.resolve($response).then(response => { let status = [ 'HTTP/1.1', response.status, response.statusText ].join(' '); let headers = [ ...response.headers.entries() ].map(this.stringifyHeader).join('\n'); let contentType = response.headers.get('Content-Type'); let json = /json/.test(contentType); let javascript = /javascript/.test(contentType); let binary = !(/text/.test(contentType) || json || javascript); let image = /image/.test(contentType); let contentDisposition = response.headers.get('Content-Disposition') || ''; try { contentDisposition = ContentDispositionAttachment.parse(contentDisposition); } catch (error) { contentDisposition = { attachment: false }; } let { attachment, filename } = contentDisposition; let $body; if (image) { $body = response.blob().then(URL.createObjectURL).then(url => { return { elements: [ new ResponseViewImage({ url, filename }) ] }; }); } else if (binary || attachment) { $body = response.blob().then(URL.createObjectURL).then(url => { return { elements: [ new ResponseViewDownloadLink({ url, filename }) ] }; }); } else { $body = response.text().then(result => { let body = result; if (json) { try { body = jsonFormatSafely(body); } catch (e) { /* ignore */ } } return { body }; }); } $body.then(({ body, elements = [] }) => { this.block.update([ status, headers, '', body ].join('\n'), ...elements); }) }).catch(({ message }) => { this.block.update(message); }); } stringifyHeader([ key, value ]) { key = key.replace(/(?:^|-)./g, $0 => $0.toUpperCase()); return [ key, value ].join(': '); } get styleSheet() { return ` :scope { height: 100%; overflow: auto; &.loading { position: relative; &:after { position: absolute; left: 3px; right: 3px; top: 3px; bottom: 3px; content: ''; background: rgba(255, 255, 255, 0.8) url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHdpZHRoPScyNHB4JyBoZWlnaHQ9JzI0cHgnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjN2NjZmFmIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InN0cm9rZS1kYXNob2Zmc2V0IiBkdXI9IjJzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgZnJvbT0iMCIgdG89IjUwMiI+PC9hbmltYXRlPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InN0cm9rZS1kYXNoYXJyYXkiIGR1cj0iMnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjE1MC42IDEwMC40OzEgMjUwOzE1MC42IDEwMC40Ij48L2FuaW1hdGU+PC9jaXJjbGU+PC9zdmc+') 50% 50% no-repeat; backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); cursor: progress; } } } `; } } class ResultView extends Jinkela { get padding() { return 0; } get btnWidth() { return 120; } get template() { return ` <dl> <dt on-click="{click}"> <span ref="underline"></span> <a href="JavaScript:" data-binding="ResponseView" data-nth="0">Response</a> <a href="JavaScript:" data-binding="RequestView" data-nth="1">Request</a> </dt> <dd> <meta ref="list" /> </dd> </dl> `; } click({ target }) { let binding = target.dataset.binding; if (!binding) return; this.underline.style.left = this.padding + target.dataset.nth * this.btnWidth + 'px'; this.list.forEach(item => { item.element.style.display = item.constructor.name === binding ? 'block' : 'none'; }); } get requestView() { let value = new RequestView(); Object.defineProperty(this, 'requestView', { value, configurable: true }); return value; } get responseView() { let value = new ResponseView(); Object.defineProperty(this, 'responseView', { value, configurable: true }); return value; } init() { this.list = [ this.requestView, this.responseView ]; this.element.querySelector('a').click(); } set waiting(value) { if (value) { this.element.classList.add('waiting'); } else { this.element.classList.remove('waiting'); } } get waiting() { return this.element.classList.has('waiting'); } update(request, $response) { this.waiting = false; this.requestView.update(request); return loader(this, this.responseView.update($response)); } get styleSheet() { return ` :scope { position: relative; height: 100%; box-sizing: border-box; margin: 0; display: flex; flex-direction: column; > dt { position: relative; border-bottom: 1px solid #e4e4ec; padding: 0 ${this.padding}px; margin: 0; font-size: 0; > a { position: relative; z-index: 1; text-decoration: none; display: inline-block; box-sizing: border-box; padding: .5em 1em; font-size: 16px; width: ${this.btnWidth}px; text-align: center; font-weight: 600; color: #5d6576; &:hover { opacity: .8; } } > span { transition: left 200ms ease, right 200ms ease; position: absolute; top: 0; bottom: -1px; font-size: 16px; width: ${this.btnWidth}px; border-bottom: 2px solid #2d2f3b; background: #f1f1f6; } } > dd { flex: 1; overflow: auto; margin: 0; } &.loading:after { position: absolute; left: 0; right: 0; top: 0; bottom: 0; content: ''; background: rgba(248, 248, 250, 0.8) url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHdpZHRoPScyNHB4JyBoZWlnaHQ9JzI0cHgnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjN2NjZmFmIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InN0cm9rZS1kYXNob2Zmc2V0IiBkdXI9IjJzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgZnJvbT0iMCIgdG89IjUwMiI+PC9hbmltYXRlPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InN0cm9rZS1kYXNoYXJyYXkiIGR1cj0iMnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjE1MC42IDEwMC40OzEgMjUwOzE1MC42IDEwMC40Ij48L2FuaW1hdGU+PC9jaXJjbGU+PC9zdmc+') 50% 50% no-repeat; cursor: progress; z-index: 1000; } &.waiting:after { position: absolute; left: 0; top: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; content: 'Click “Send” to send the request'; font-weight: 500; font-size: 1.2em; background: rgba(248, 248, 250, 0.8); z-index: 1000; } } `; } } class SideBarInput extends Jinkela { get template() { return ` <div> <input ref="input" placeholder="{placeholder}" /> </div> `; } get placeholder() { return ''; } set value(value) { this.input.value = value; } get value() { return this.input.value; } select() { this.input.select(); } get styleSheet() { return ` :scope { padding: 0 1em; display: flex; box-sizing: border-box; color: #dbdfeb; background: #3f4555; line-height: 40px; border: 0 none; border-radius: 8px; > input { margin: 0; padding: 0; flex: 1; border: 0 none; color: inherit; background: transparent; font: inherit; vertical-align: middle; outline: none; &::-webkit-input-placeholder { color: #5d6577; } } } `; } } class SideBarMethodSelect extends Jinkela { get template() { return ` <select> <option>GET</option> <option>POST</option> <option>PUT</option> <option>PATCH</option> <option>DELETE</option> </select> `; } set value(value) { this.element.value = value; } get value() { return this.element.value; } get styleSheet() { return ` :scope { padding: 0 1.5em 0 1em; color: #dbdfeb; background: #3f4555; font: inherit; min-width: 40px; max-width: 100px; height: 40px; border: 0 none; border-radius: 8px; outline: none; -webkit-appearance: none; -moz-appearance: none; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAqdJREFUeNpi/P//P8NAAiaGAQajDhh1wKgDGEDlADqGAnYgNoLS5AI2IDaEmYHVLhwOYFVT1/HOLqzdoaKm5Q3ik2E5q5KyumdWQc02dU1dPxAfm11Yo0BJRcPOLzCqUUBASC8gKKZRQUnViURHsIL0BIXGNwoKChv4BUTVAx1jT3QaiIhOy+fh45diZ+dgANEhEclNcgrKDkQ6glVGVsEOqKcBqFcGZAY3L59kREx6PtEOYOdgb2RlY/vKzsHBwAHEfHz8smGRKY1S0nLWQGkWPJazgNSER6c1AfXIg/SCzACa9R1oZhM2DYzYKiNGRkaGrokLLViYWZYxMDJw/P71i+Hnzx8MHz68v7ts4bSaF8+fHAEq+4umjVlMXNIyJiGnTUBQSAXkc6DFwETG8OPP3z8xZfnxx7DZhTMbAjWcYGRijGdiYvrJxsbOADJQQEBQOTIuo1FYWNQCZCGy5cIiYubR8dlNyJYD9f4GmpEIspyscqAwK+ow0AfJQEN+gwxkY2dnACYqtdjk3CaQhVD9TEJCIqYxidlNgkLCGiA1MMtBeoFmHMRnB84oQAYTpi13B0bFrH///rH++vWT4dfPnwxv3766PHdGbw0oeyenF7cICYvqgeIbFFpAy/8ALU8vyIrcjl7mkOUAqCN8gCEx/e/fv8wwR7x7++oC0FF/RUQljEE+B1nOzMz89/+//9lAyzdhK/TIdgDUEYFAR0wCOQKUMH///gVJ+qysyJYXAS1fjavUpcgBUEeEAx3R9+/vP8a//yAZgZmJmYGJmek/0PISoOXL8RX7FDsA7IjpK2KBajpAShFm/68uyIxYQKjeoUptCLRoMdCwemBiAyU4kNENhCynKBfgiY4MUAwAg30qsTUvUQ4YbRGNOmDUAaMOoCcACDAAFHYu4lUVtcQAAAAASUVORK5CYII='); background-position: 90% 50%; background-repeat: no-repeat; background-size: 16px; cursor: pointer; &:hover { color: #fff; } } `; } } class SideBarQueryStringInput extends SideBarInput { get styleSheet() { return ` :scope { flex: 1; &:before { color: #5d6577; } } `; } } class SideBarSendButton extends Jinkela { get template() { return '<button>Send</button>'; } get styleSheet() { return ` :scope { padding: 0 1em; height: 40px; color: #dbdfeb; background: transparent; font: inherit; border: 2px solid #959cad; border-radius: 4px; outline: none; cursor: pointer; transition: .15s background; &:hover { color: #fff; border-color: #fff; } } `; } } class SideBarMethod extends Jinkela { get template() { return ` <div> <jkl-side-bar-method-select ref="methodSelect" on-change="{methodChange}"></jkl-side-bar-method-select> <jkl-side-bar-query-string-input ref="queryStringInput" on-input="{queryStringChange}" placeholder="URL or query string"></jkl-side-bar-query-string-input> <jkl-side-bar-send-button></jkl-side-bar-send-button> </div> `; } set method(value) { this.methodSelect.value = value; } get method() { return this.methodSelect.value; } set queryString(value) { this.queryStringInput.value = value; } get queryString() { let value = this.queryStringInput.value; if (value.match(new RegExp(`^https?://`)) || value.match(new RegExp(`^/`))) { let [, url = value, query] = value.match(/(.*)\?(.*)/) || [] value = query ? query : ``; if (url.match(new RegExp(`^//`))) { url = `${location.protocol}${url}` } else if(url.match(new RegExp(`^/`))) { url = `${location.origin}${url}` } this.url = url } return value; } methodChange() { this.element.dispatchEvent(new CustomEvent('methodchange', { bubbles: true, detail: this.method })); } queryStringChange() { this.element.dispatchEvent(new CustomEvent('querystringchange', { bubbles: true, detail: this.queryString })); } get styleSheet() { return ` :scope { margin: -.25em; display: flex; & > * { margin: .25em; } } `; } } class SideBarKeyValuePairCheckBox extends Jinkela { get template() { return ` <input type="checkbox" /> `; } set checked(value) { this.element.checked = value; } get checked() { return this.element.checked; } get styleSheet() { return ` :scope { margin: 0 8px 0 10px; width: 18px; height: 18px; -webkit-appearance: none; -moz-appearance: none; background: #2d2f3b; border-radius: 4px; cursor: pointer; outline: none; &:hover { background: #262c39; } &:checked { background-position: center center; background-repeat: no-repeat; background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSI2IiB2aWV3Qm94PSIwIDAgOCA2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Ik0xIDIuOTRsMi4wMyAyLjAzTDcgMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+'); } } `; } } class SideBarKeyValuePairInput extends SideBarInput { get styleSheet() { return ` :scope { padding: 4px 6px 4px 0; flex: 1; border-radius: 0; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 32px; } `; } } class SideBarKeyValuePair extends Jinkela { get template() { return ` <div> <jkl-side-bar-key-value-pair-check-box ref="enabledCheckBox" on-click="{emitChange}"> </jkl-side-bar-key-value-pair-check-box> <jkl-side-bar-key-value-pair-input placeholder="key" ref="keyInput" on-input="{emitChange}"> </jkl-side-bar-key-value-pair-input> <jkl-side-bar-key-value-pair-input placeholder="value" ref="valueInput" on-input="{emitChange}"> </jkl-side-bar-key-value-pair-input> </div> `; } set enabled(value) { this.enabledCheckBox.checked = value; } get enabled() { return this.enabledCheckBox.checked; } set key(value) { this.keyInput.value = value; } get key() { return this.keyInput.value; } set value(value) { this.valueInput.value = value; } get value() { return this.valueInput.value; } select() { this.keyInput.select(); } emitChange() { this.element.dispatchEvent(new CustomEvent('pairchange', { bubbles: true })); } get styleSheet() { return ` :scope { margin-bottom: 1px; display: flex; align-items: center; background: #3f4555; font-size: 12px; } `; } } class SideBarKeyValueAdd extends Jinkela { get template() { return ` <div> <span ref="children"></span> </div> `; } get styleSheet() { return ` :scope { padding: 0 11px 0 35px; display: flex; align-items: center; height: 40px; color: #5d6577; background: #3f4555 url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiI+PHBhdGggZD0iTTUgMTJWN0gwVjVoNVYwaDJ2NWg1djJIN3Y1eiIgZmlsbD0iIzVkNjU3NyIvPjwvc3ZnPg==') 12px 50% no-repeat; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; cursor: pointer; &:hover { color: #fff; } } `; } } class SideBarKeyValueCollection extends Jinkela { get template() { return ` <div> <div ref="list"></div> <div> <jkl-side-bar-key-value-add on-click="{add}">{text}</jkl-side-bar-key-value-add> </div> </div> `; } get text() { return 'add'; } fromJSON(json) { while (this.list.firstChild) this.list.firstChild.remove(); this.$collection = SideBarKeyValuePair .from(json.map(({ enabled = true, key, value }) => ({ enabled, key, value }))) .to(this.list); } toJSON() { return (this.$collection || []) .map(({ enabled, key, value }) => ({ enabled, key, value })); } set collection(value) { this.fromJSON(value); } get collection() { return this.toJSON() .filter(({ enabled }) => enabled) .map(({ key, value }) => ({ key, value })); } add() { let item = new SideBarKeyValuePair({ enabled: true, key: 'new item' }).to(this.list); item.select(); (this.$collection || (this.$collection = [])).push(item); this.element.dispatchEvent(new CustomEvent('collectionchange', { bubbles: true })); } get styleSheet() { return ` :scope { > * { border-radius: 4px; overflow: hidden; } } `; } } class SideBarBodyEditor extends Jinkela { get template() { return ` <div> <h3>Body</h3> </div> `; } init() { this.cm = CodeMirror(this.element, { mode: 'application/json', theme: 'material' }); } didMount() { this.refresh(); } set value(value) { this.cm.setValue(value); } get value() { return this.cm.getValue(); } refresh() { setTimeout(() => this.cm.refresh(), 0); } get styleSheet() { return ` :scope { .CodeMirror { padding: 1em; height: 10em; border-radius: 8px; background: #3f4555; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 1.5; } } `; } } class SideBarHeaderCollection extends SideBarKeyValueCollection { get value() { return this.collection .reduce((result, { key, value }) => { (result[key] || (result[key] = [])).push(value); return result; }, {}); } } class SideBar extends Jinkela { get template() { return ` <form on-submit="{submit}"> <jkl-side-bar-method ref="methodComponent" on-methodchange="{methodChange}" on-querystringchange="{syncQueryParameters}"> </jkl-side-bar-method> <h3>Query Parameters</h3> <jkl-side-bar-key-value-collection ref="queryParameterCollection" text="add query parameter" on-collectionchange="{syncQueryString}" on-pairchange="{syncQueryString}"> </jkl-side-bar-key-value-collection> <jkl-side-bar-body-editor ref="bodyEditor"></jkl-side-bar-body-editor> <h3>Headers</h3> <jkl-side-bar-header-collection ref="headerCollection" text="add header"> </form> `; } set method(value) { this.methodComponent.method = value; this.methodChange(); } get method() { return this.methodComponent.method; } set queryString(value) { this.methodComponent.queryString = value; this.syncQueryParameters(); } get queryString() { return this.methodComponent.queryString; } set queryParameters(value) { this.queryParameterCollection.collection = value; this.syncQueryString(); } get queryParameters() { return this.queryParameterCollection.collection; } set body(value) { return this.bodyEditor.value = value; } get body() { return this.bodyEditor.value; } set headers(value) { this.headerCollection.collection = value; } get headers() { return this.headerCollection.value; } fromJSON(json) { let { method, queryParameters, body, headers, uri } = json; this.uri = uri; if (method) this.method = method.toUpperCase(); if (queryParameters) { this.queryParameters = queryParameters; } else { this.queryString = location.search.slice(1); } if (body) this.body = body; if (headers) this.headerCollection.fromJSON(headers); } toJSON() { let { method, body, uri } = this; let queryParameters = this.queryParameterCollection.toJSON(); let headers = this.headerCollection.toJSON(); return { method, queryParameters, body, headers, uri }; } set isBodyVisible(value) { if (value) { this.bodyEditor.element.style.display = 'block'; this.bodyEditor.refresh(); } else { this.bodyEditor.element.style.display = 'none'; } } get isBodyVisible() { return this.bodyEditor.element.style.display !== 'none'; } init() { let { method, queryParameters = [], body, headers = [], uri, url} = QueryString.parse(location.hash.replace(/^#!?/, '')); uri = uri || url || `${location.protocol}//${location.host}${location.pathname}` try { headers = JSON.parse(headers); } catch (error) { /* ignore */ } try { queryParameters = JSON.parse(queryParameters); } catch (error) { /* ignore */ } this.fromJSON({ method, queryParameters, body, headers, uri }); this.methodChange(); } submit(event) { event.preventDefault(); let { method, queryParameters, body, headers, uri } = this.toJSON(); if (queryParameters) queryParameters = JSON.stringify(queryParameters); if (headers) headers = JSON.stringify(headers); let hash = QueryString.stringify({ method, queryParameters, body, headers, uri }); if (hash) hash = '#!' + hash; if (location.hash !== hash) { location.hash = hash; } else { dispatchEvent(new Event('hashchange')); } } methodChange() { this.isBodyVisible = isBodyEnabled(this.method); } syncQueryParameters() { let { queryString } = this; this.uri = this.methodComponent.url || this.uri let queryParameters = QueryString.parse(queryString, true); this.queryParameterCollection.collection = queryParameters; } syncQueryString() { let { collection } = this.queryParameterCollection; let queryString = QueryString.stringify(collection); this.methodComponent.queryString = `${this.uri}${queryString ? `?${queryString}` : ``}`; } get styleSheet() { return ` :scope { padding: 40px; box-sizing: border-box; color: #fff; background: #2d2f3b; transition: transform 1s; height: 100%; box-sizing: border-box; overflow: auto; h3 { margin-left: -40px; margin-right: -40px; padding: 20px 40px; border-bottom: 1px solid #373b48; font-size: 17px; font-weight: 600; } } `; } } class Main extends Jinkela { get template() { return ` <div class="gMain"> <section> <jkl-result-view ref="resultView"></jkl-result-view> </section> <aside> <jkl-side-bar ref="sideBar"></jkl-side-bar> </aside> </div> `; } init() { if (this.sideBar.method === 'GET' || this.sendByUser) { this.sendRequest(); } else { this.resultView.waiting = true; } Configuration.watch('settings.sidebar.collapse', value => { if (value) { this.element.classList.add('sidebar-collapse'); } else { this.element.classList.remove('sidebar-collapse'); } }); } sendRequest() { let { method, queryString, body, headers, uri } = this.sideBar; let path = new URL(uri).pathname let defaultHeaders = { Accept: 'application/json, */*', 'Content-Type': 'application/json' }; headers = Object.assign({}, defaultHeaders, headers); if (queryString) uri += '?' + queryString; let options = { method, headers, credentials: 'include' }; if (this.sideBar.isBodyVisible) options.body = body; let $response = fetch(uri, options); this.resultView.update({ method, path, queryString, body, headers }, $response); // sync title document.title = [ method, uri ].join(' '); } get styleSheet() { return ` :scope { position: relative; flex: 1; width: 100%; min-height: 0; > section, > aside { position: absolute; top: 0; bottom: 0; } > section { left: 0; width: 57%; transition: width .01s .7s; } > aside { left: 57%; right: 0; transition: transform .7s; } &.sidebar-collapse { > section { width: 100%; transition: none; } > aside { transform: translateX(100%); } } } `; } } class Frame extends Jinkela { init() { let { sendByUser } = this; new Main({ sendByUser }).to(this); } get styleSheet() { return ` html { height: 100%; overflow: hidden; } @media screen and (max-width: 750px) { html { overflow: scroll; } html body .gMain { display: flex; flex-wrap: wrap; } html body .gMain > section, html body .gMain > aside { width: 100%; position: initial; } html body .gMain > section { order: 2; } html body .gMain > aside { order: 1; } html body .gMain > aside > form { overflow: hidden; padding: 20px; } } body { margin: 0; height: 100%; background: #fff; color: #4c555a; font-family: "Alright Sans LP", "Avenir Next", "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Source Han Sans SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi MicroHei", sans-serif; font-size: 14px; -webkit-font-smoothing: antialiased; line-height: 26px; } pre, code { font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } :scope { display: flex; flex-direction: column; width: 100%; height: 100%; } `; } } let frame = new Frame(); addEventListener('hashchange', () => frame = new Frame({ sendByUser: true }).renderWith(frame)); addEventListener('DOMContentLoaded', () => frame.to(document.body)); </script> </head> </html>