@advanced-rest-client/raw-payload-editor
Version:
A raw payload input editor based on CodeMirror
366 lines (335 loc) • 9.9 kB
JavaScript
/* eslint-disable no-param-reassign */
/* eslint-disable class-methods-use-this */
/**
@license
Copyright 2019 The Advanced REST client authors <arc@mulesoft.com>
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
*/
import { LitElement, html, css } from 'lit-element';
import { ArcResizableMixin } from '@advanced-rest-client/arc-resizable-mixin';
import { EventsTargetMixin } from '@advanced-rest-client/events-target-mixin';
import { PayloadParserMixin } from '@advanced-rest-client/payload-parser-mixin';
import '@polymer/paper-toast/paper-toast.js';
import '@anypoint-web-components/anypoint-button/anypoint-button.js';
import '@advanced-rest-client/code-mirror/code-mirror.js';
import '@advanced-rest-client/code-mirror-linter/code-mirror-linter.js';
import linterStyles from '@advanced-rest-client/code-mirror-linter/lint-style.js';
/**
* A raw payload input editor based on CodeMirror.
*
* The element additionally shows Encode / Decode buttons if current content type value contains
* "x-www-form-urlencoded".
*
* The element listens for `content-type-changed` custom event and updates the `contentType` property
* automatically. This event is commonly used in ARC elements.
*
* ### Example
* ```
* <raw-payload-editor content-type="application/json"></raw-payload-editor>
* ```
*/
class RawPayloadEditor extends ArcResizableMixin(PayloadParserMixin(EventsTargetMixin(LitElement))) {
get styles() {
return [
linterStyles,
css`:host {
display: block;
}
.action-buttons > * {
margin: 8px 0;
}
*[hidden] {
display: none !important;
}
code-mirror {
height: auto;
max-height: 300px;
}
anypoint-button,
.action-buttons ::slotted(anypoint-button) {
white-space: nowrap;
}
`
];
}
render() {
const { _encodeEnabled, _isJson, lineNumbers, compatibility } = this;
const gutters = ['CodeMirror-lint-markers'];
return html`<style>${this.styles}</style>
<div class="action-buttons">
${_encodeEnabled ? html`
<anypoint-button
data-action="encode"
="${this.encodeValue}"
emphasis="medium"
title="Encodes payload to x-www-form-urlencoded data"
?compatibility="${compatibility}"
>Encode payload</anypoint-button>
<anypoint-button
data-action="decode"
="${this.decodeValue}"
emphasis="medium"
title="Decodes payload to human readable form"
?compatibility="${compatibility}"
>Decode payload</anypoint-button>
` : undefined}
${_isJson ? html`
<anypoint-button
data-action="format-json"
emphasis="medium"
="${this.formatValue}"
title="Formats JSON input."
?compatibility="${compatibility}">Format JSON</anypoint-button>
<anypoint-button
data-action="minify-json"
="${this.minifyValue}"
emphasis="medium"
title="Removed whitespaces from the input"
?compatibility="${compatibility}">Minify JSON</anypoint-button>
` : undefined}
<slot name="content-action"></slot>
</div>
<code-mirror
mode="application/json"
-changed="${this._editorValueChanged}"
.gutters=${gutters}
?lineNumbers="${lineNumbers}"
></code-mirror>
<paper-toast id="invalidJsonToast">JSON value is invalid. Cannot parse value.</paper-toast>`;
}
static get properties() {
return {
/**
* Raw payload value
*/
value: { type: String },
/**
* Renders line number when set.
* @type {Object}
*/
lineNumbers: { type: Boolean },
/**
* Enables compatibility mode with Anypoint platform.
*/
compatibility: { type: Boolean },
/**
* Content-Type header value. Determines current code mirror mode.
*/
contentType: { type: String },
// Computed value, true if `contentType` contains `x-www-form-urlencoded`
_encodeEnabled: { type: Boolean },
// Computed value, true if `contentType` contains `/json`
_isJson: { type: Boolean }
};
}
get _editor() {
return this.shadowRoot.querySelector('code-mirror');
}
get value() {
return this._value;
}
set value(value) {
const old = this._value;
if (old === value) {
return;
}
this._value = value;
this._valueChanged(value);
this.dispatchEvent(new CustomEvent('value-changed', {
detail: {
value
}
}));
}
get contentType() {
return this._contentType;
}
set contentType(value) {
const old = this._contentType;
if (old === value) {
return;
}
this._contentType = value;
this._onContentTypeChanged(value);
this._encodeEnabled = this.__computeIs(value, 'x-www-form-urlencoded');
this._isJson = this._computeIsJson(value);
}
get onvalue() {
return this._onValue;
}
set onvalue(value) {
if (this._onValue) {
this.removeEventListener('value-changed', this._onValue);
}
if (typeof value !== 'function') {
this._onValue = null;
return;
}
this._onValue = value;
this.addEventListener('value-changed', this._onValue);
}
constructor() {
super();
this._contentTypeHandler = this._contentTypeHandler.bind(this);
this._resizeHandler = this._resizeHandler.bind(this);
}
_attachListeners(node) {
node.addEventListener('content-type-changed', this._contentTypeHandler);
this.addEventListener('iron-resize', this._resizeHandler);
}
_detachListeners(node) {
node.removeEventListener('content-type-changed', this._contentTypeHandler);
this.removeEventListener('iron-resize', this._resizeHandler);
}
firstUpdated() {
this.refresh();
if (this.contentType) {
this._onContentTypeChanged(this.contentType);
}
if (this.value) {
this._valueChanged(this.value);
}
}
/**
* Forces render code-mirror content.
* Should be used to when the element becomes visible after being hidden.
*/
refresh() {
this._editor.refresh();
}
/**
* Changes CodeMirror mode when the content type value is updated.
*
* @param {String} ct
*/
_onContentTypeChanged(ct) {
const editor = this._editor;
if (!editor) {
return;
}
this._setupLinter(ct);
if (!ct) {
return;
}
if (ct.indexOf && ct.indexOf(';') !== -1) {
ct = ct.substr(0, ct.indexOf(';'));
}
this._editor.mode = ct;
}
_computeIsJson(ct) {
return this.__computeIs(ct, '/json');
}
__computeIs(ct, needle) {
if (!ct) {
return false;
}
if (ct.indexOf && ct.indexOf(needle) !== -1) {
return true;
}
return false;
}
/**
* Handler for the `content-type-changed` event. Sets the `contentType` property.
*
* @param {CustomEvent} e
*/
_contentTypeHandler(e) {
this.contentType = e.detail.value;
}
/**
* Handler for value change.
* If the element is opened then it will fire change event.
*
* @param {String} value
*/
_valueChanged(value) {
const editor = this._editor;
if (this.__editorValueChange || !editor) {
return;
}
editor.value = value;
}
/**
* Called when the editor fires change event
*
* @param {CustomEvent} e
*/
_editorValueChanged(e) {
e.stopPropagation();
this.__editorValueChange = true;
this.value = e.detail.value;
this.__editorValueChange = false;
}
_setupLinter(ct) {
/* global CodeMirror */
const editor = this._editor;
if (this._computeIsJson(ct) && CodeMirror.lint) {
editor.lint = CodeMirror.lint.json;
editor.gutters = ['code-mirror-lint', 'CodeMirror-lint-markers'];
} else {
editor.lint = false;
editor.gutters = ['CodeMirror-lint-markers'];
}
editor.refresh();
}
/**
* Formats current value as it is a JSON object.
*/
formatValue() {
try {
let {value} = this;
value = JSON.parse(value);
value = JSON.stringify(value, null, 2);
this.value = value;
this.refresh();
} catch (e) {
this.shadowRoot.querySelector('#invalidJsonToast').opened = true;
}
}
/**
* Minifies JSON value by removing whitespaces.
*/
minifyValue() {
try {
let {value} = this;
value = JSON.parse(value);
value = JSON.stringify(value);
this.value = value;
this.refresh();
} catch (e) {
this.shadowRoot.querySelector('#invalidJsonToast').opened = true;
}
}
_resizeHandler() {
this.refresh();
}
/**
* URL encodes payload value and resets the same value property.
* This should be used only for payloads with x-www-form-urlencoded content-type.
*/
encodeValue() {
const value = this.encodeUrlEncoded(this.value);
this.__internalChange = true;
this.value = value;
this.__internalChange = false;
}
/**
* URL decodes payload value and resets the same value property.
* This should be used only for payloads with x-www-form-urlencoded content-type.
*/
decodeValue() {
const value = this.decodeUrlEncoded(this.value);
this.__internalChange = true;
this.value = value;
this.__internalChange = false;
}
}
window.customElements.define('raw-payload-editor', RawPayloadEditor);