mockm
Version:
Analog interface server, painless parallel development of front and back ends.
1,085 lines (1,060 loc) • 38.9 kB
HTML
<!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;
}
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>