api-console-assets
Version:
This repo only exists to publish api console components to npm
807 lines (754 loc) • 24.1 kB
HTML
<!--
@license
Copyright 2017 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.
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../paper-input/paper-input-container.html">
<link rel="import" href="../paper-input/paper-input-behavior.html">
<link rel="import" href="../paper-input/paper-input-error.html">
<link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html">
<link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html">
<link rel="import" href="../marked-element/marked-element.html">
<link rel="import" href="../markdown-styles/markdown-styles.html">
<link rel="import" href="../paper-styles/typography.html">
<link rel="import" href="../iron-input/iron-input.html">
<link rel="import" href="../events-target-behavior/events-target-behavior.html">
<!--
`<raml-request-url-editor>` An URL editor for the RAML request panel.
The element renders an input that produces final URL for a request.
It expects the `base-uri` and `endpoint-uri` properties to be set to produce any
output. This properties can be read from the RAML.
The editor works with `raml-request-parameters-model` element. URI and query
parameters model computed by the `raml-request-parameters-model` element.
When set it adds support for updating query and URI parameters from other
parts of the application (like `raml-request-parameters-editor`).
If the models are not set then this element behaves as regular input.
### Example of use (Polymer app)
```html
<raml-request-url-editor auto-validate required value="{{url}}"
base-uri="[[raml.baseUri]]"
endpoint-uri="[[raml.selectedMethod.relativeUri]]"
query-model="[[qm]]"
uri-model="[[um]]"></raml-request-url-editor>
<raml-request-parameteres-model
query-parameters="[[raml.queryParameters]]"
uri-parameters="[[raml.allUriParameters]]"
query-model="{{qm}}"
uri-model="{{um}}"></raml-request-parameteres-model>
<script>
var input = document.querySelector('raml-request-url-editor');
input.addEventListener('value-changed', function(e) {
if (input.validate()) {
var url = e.detail.value;
console.log(url);
}
});
</script>
```
### Example for pure JavaScript
```html
<raml-request-url-editor auto-validate required id="editor"></raml-request-url-editor>
<raml-request-parameteres-model id="model"></raml-request-parameteres-model>
<script>
var model = document.getElementById('model');
var editor = document.getElementById('url');
function passDataToView(e) {
var type = e.type;
type = type.replace('-changed', '');
var property = type.replace(/-[a-z]/g, function(m) {
return m[1].toUpperCase();
});
editor[property] = e.detail.value;
}
model.addEventListener('query-model-changed', passDataToView);
model.addEventListener('uri-model-changed', passDataToView);
editor.addEventListener('value-changed', function(e) {
console.log(e.detail.value);
});
var raml = await getRamlSomehow();
var selected = raml.resources[0].methods[0];
model.queryParameters = selected.queryParameters;
model.uriParameters = selected.allUriParameters;
editor.baseUri = raml.baseUri;
editor.endpointUri = selected.relativeUri;
</script>
```
The element listens for `url-value-changed` event and updated the URL value
from event's detail object.
```javascript
document.dispatchEvent(new CustomEvent('url-value-changed', {
bubbles: true,
detail: {
value: 'http://www.domain.com'
}
}));
```
Firing this event updates the value to `http://www.domain.com`.
### Validation
This element implements `Polymer.IronValidatableBehavior`. You can call `validate()`
function to check if the form is valid.
An attribute `invalid` is set if the form is invalid. It can be used for
styling. The form is invalid if the value contains URL variables.
### Styling
`<raml-request-url-editor>` provides the following custom properties and mixins for styling:
Custom property | Description | Default
----------------|-------------|----------
`--raml-request-url-editor` | Mixin applied to the element | `{}`
`--raml-request-url-editor-documentation` | Mixin applied to the documentation field. Not that this node has the `--arc-font-body1` mixin and also `markdown-styles` applies. | `{}`
Additionally use styles defined for the `paper-input` element.
@group RAML Elements
@element raml-request-url-editor
@demo demo/index.html
-->
<dom-module id="raml-request-url-editor">
<template>
<style include="markdown-styles"></style>
<style>
:host {
outline: none;
display: block;
@apply(--raml-request-url-editor);
}
.markdown-body {
@apply(--arc-font-body1);
margin-top: 12px;
color: rgba(0, 0, 0, 0.54);
@apply(--raml-request-url-editor-documentation);
}
.paper-input-input {
@apply(--raml-request-url-editor-input);
}
</style>
<paper-input-container
no-label-float="[[noLabelFloat]]"
attr-for-value="bind-value"
always-float-label="[[alwaysFloatLabel]]"
disabled$="[[disabled]]"
invalid="[[invalid]]">
<label hidden$="[[!label]]">[[label]]</label>
<input is="iron-input"
id="input"
class="paper-input-input"
aria-label-prefix="[[_ariaLabelledBy]]"
bind-value="{{value}}"
disabled$="[[disabled]]"
required$="[[required]]"
autofocus$="[[autofocus]]"
readonly$="[[readonly]]"
type="url"
on-tap="_inputTap"
on-blur="_onElementBlur"
on-input="__userInputHandler"/>
<template is="dom-if" if="[[errorMessage]]">
<paper-input-error>[[errorMessage]]</paper-input-error>
</template>
</paper-input-container>
<template is="dom-if" if="[[documentation]]">
<marked-element markdown="[[documentation]]">
<div class="markdown-html markdown-body"></div>
</marked-element>
</template>
</template>
<script>
Polymer({
is: 'raml-request-url-editor',
behaviors: [
Polymer.PaperInputBehavior,
Polymer.IronValidatableBehavior,
Polymer.IronFormElementBehavior,
ArcBehaviors.EventsTargetBehavior
],
/**
* Fired when the URL value change.
* Note that this event is fifed before validation occur and therefore the URL may be invalid.
*
* @event url-value-changed
* @param {String} value The URL.
*/
properties: {
/**
* The label for this input.
*/
label: {
type: String,
value: 'Request URL'
},
// A value generated by this editor - the URL.
value: {
type: String,
notify: true,
observer: '_onValueChanged'
},
// An error message to display
errorMessage: {
type: String,
value: 'Fill the URI parameters before making a request'
},
// A documentation to show.
documentation: {
type: String
},
// If true it will not display documentation for the variable (if available).
skipDocs: Boolean,
/**
* Value or RAML's base URI property.
*
* Note, the element doesn't check if `baseUri` is relative or not.
* Hosting application have to take care of that.
*/
baseUri: String,
/**
* Currently selected endpoint relative URI. It is available in RAML
* definition.
*/
endpointUri: String,
// Computed value, sum of `baseUri` and `endpointUri`
_fullUri: {
computed: '_computeFullUrl(baseUri, endpointUri)'
},
/**
* Computed query properties model.
* Use `raml-request-parameteres-model` to compute model for the view.
*/
queryModel: {
type: Array,
value: function() {
return [];
}
},
/**
* Computed URI properties model.
* Use `raml-request-parameteres-model` to compute model for the view.
*/
uriModel: {
type: Array,
value: function() {
return [];
}
},
/**
* Computed, ordered list of URL variables in the URI string.
*/
urlParams: {
type: Array,
computed: '_computeUrlParams(_fullUri)'
},
/**
* Computed regexp for the current `_fullUri` value to search for the
* URI parameters.
*/
urlSearchRegexp: {
type: RegExp,
computed: '_computeUrlRegexp(_fullUri)'
},
},
hostAttributes: {
'tabindex': -1,
focusable: true
},
observers: [
'_computeValueChanged(_fullUri, queryModel.*, uriModel.*)',
'_notifyModelChanged(queryModel.*)',
'_notifyModelChanged(uriModel.*)'
],
ready: function() {
this._elementReady = true;
// If there's an initial input, validate it.
if (this.value) {
this._handleAutoValidate();
}
},
_attachListeners: function(node) {
this.listen(node, 'url-value-changed', '_extValueChangedHandler');
},
_detachListeners: function(node) {
this.unlisten(node, 'url-value-changed', '_extValueChangedHandler');
},
/**
* Computes endpoint's full URI with (possibly) variables in it.
*
* @param {String} baseUri API base URI
* @param {String} endpointUri Endpoint relative URI to `baseUri`
* @return {String} A full URI for the endpoint.
*/
_computeFullUrl: function(baseUri, endpointUri) {
if (!endpointUri) {
endpointUri = '/';
}
if (endpointUri[0] !== '/') {
endpointUri = '/' + endpointUri;
}
if (!baseUri) {
return endpointUri;
}
if (baseUri[baseUri.length - 1] === '/') {
baseUri = baseUri.substr(0, baseUri.length - 1);
}
return baseUri + endpointUri;
},
_computeValueChanged: function(_fullUri, queryRecord, uriRecord) {
this._computeValue(queryRecord.base, uriRecord.base, _fullUri);
},
/**
* Computes url value from current `baseUri` and query/uri models.
*
* @param {Array} queryModel Query parameters model
* @param {Array} uriModel Uri parameters model.
* @param {String} uri Current endpoint uri.
*/
_computeValue: function(queryModel, uriModel, uri) {
if (!uri) {
this.set('value', undefined);
return;
}
uri = this._applyUriParams(uri, uriModel);
uri = this._applyQueryParams(uri, queryModel);
this.set('value', uri);
},
/**
* Creates a map of serialized values from a model.
* It is a replacemenet for `iron-form` serialize function which
* can't be used here because this function is called before local DOM
* is ready and therefore form is not set.
*
* @param {Array} model Model to compute.
* @return {Object} Map of serialized values.
*/
_formValuesFromModel: function(model) {
var result = {};
if (!model || !model.length) {
return result;
}
model.forEach(function(item) {
var value = item.value;
if (!value && item.required) {
value = '';
} else if (!item.required) {
if (!value && value !== 0 && value !== false && value !== null) {
value = undefined;
}
}
result[item.key] = value;
});
return result;
},
/**
* Applies URI parameters to the URL.
*
* @param {String} url An URL to apply the params to
* @param {Array} model Uri parameters model.
* @return {String} The URL.
*/
_applyUriParams: function(url, model) {
var uriParams = this._formValuesFromModel(model);
for (var paramName in uriParams) {
var value = uriParams[paramName];
if (!value) {
continue;
}
value = String(value);
if (value.trim() === '') {
continue;
}
value = this._wwwFormUrlEncodePiece(value);
var re = new RegExp('{' + paramName + '}');
url = url.replace(re, value);
}
return url;
},
/**
* Applies query parameters to the URL.
* Query parameters that are not required by the API spec and don't have value
* are removed from the URL. Parameters that are required and don't have
* value are set to the URL but with empty value.
*
* @param {String} url An URL to apply the params to
* @param {Array} model Query parameters model.
* @return {String} The URL.
*/
_applyQueryParams: function(url, model) {
var params = this._formValuesFromModel(model);
var items = this._computeQueryItems(params);
params = this._wwwFormUrlEncode(items);
if (!params) {
return url;
}
url += (url.indexOf('?') === -1) ? '?' : '&';
url += params;
return url;
},
/**
* Computes query parameters list of items containing `name` and `value`
* properties to use to build query string.
*
* This function may change the `params` map.
*
* @param {Object} params Map of query model properties.
* @return {Array} List of query parameters.
*/
_computeQueryItems: function(params) {
var items = [];
for (var paramName in params) {
var value = params[paramName];
if (value === undefined) {
continue;
}
var isArray = false;
if (value instanceof Array) {
isArray = true;
if (!value.length || (value.length === 1 && !value[0])) {
continue;
}
}
if (isArray) {
for (var i = 0, len = value.length; i < len; i++) {
if (value || value === 0 || value === false) {
items.push({
name: paramName,
value: value[i]
});
}
}
} else {
items.push({
name: paramName,
value: value
});
}
}
return items;
},
/**
* @param {Array} object The list of objects to encode as
* x-www-form-urlencoded string. Each entry should have `name` and `value`
* properties.
* @return {string} .
*/
_wwwFormUrlEncode: function(object) {
if (!object || !object.length) {
return '';
}
var pieces = object.map(function(item) {
return this._wwwFormUrlEncodePiece(item.name) + '=' +
this._wwwFormUrlEncodePiece(item.value);
}, this);
return pieces.join('&');
},
/**
* @param {*} str A key or value to encode as x-www-form-urlencoded.
* @return {string} .
*/
_wwwFormUrlEncodePiece: function(str) {
// Spec says to normalize newlines to \r\n and replace %20 spaces with +.
// jQuery does this as well, so this is likely to be widely compatible.
if (!str) {
return '';
}
return encodeURIComponent(str.toString().replace(/\r?\n/g, '\r\n'))
.replace(/%20/g, '+');
},
/**
* Updates URI / query parameters model from on user input.
*
* @param {Event} e Input event
*/
__userInputHandler: function(e) {
var value = e.target.value;
var matches;
var uriParams = this.urlParams;
var uriRegexp = this.urlSearchRegexp;
if (uriParams && uriRegexp) {
matches = value.match(uriRegexp);
if (matches) {
matches.shift();
this._applyUriValues(matches, uriParams);
}
}
matches = value.match(/[^&?]*?=[^&?]*/g);
if (!matches) {
return;
}
var params = {};
matches.forEach(function(item) {
var parts = item.split('=');
var name = parts[0];
if (name in params) {
if (!(params[name] instanceof Array)) {
params[name] = [params[name]];
}
params[name].push(parts[1]);
} else {
params[name] = parts[1];
}
});
this._applyQueryParamsValues(params);
},
/**
* Applies values from the `values` array to the uri parametes which names are in the `names`
* array.
* Both lists are ordered list of paramerters.
*
* @param {Array<String>} values Values for the parameters
* @param {Array<String>} names List of variables names (uri parameters).
*/
_applyUriValues: function(values, names) {
var up = this.uriModel;
var len = up.length;
names.forEach(function(item, index) {
for (var i = 0; i < len; i++) {
if (up[i].name === item) {
this.fire('uri-parameter-changed', {
name: up[i].name,
value: values[index]
});
break;
}
}
}, this);
},
/**
* Applies query parameters values to the render list.
*
* @param {Object} map A map where keys are names of the parameters in the
* `queryModel` list
*/
_applyQueryParamsValues: function(map) {
var qp = this.queryModel;
var len = qp.length;
for (var _key in map) {
for (var i = 0; i < len; i++) {
if (qp[i].name === _key) {
this.fire('query-parameter-changed', {
name: qp[i].name,
value: map[_key]
});
break;
}
}
}
},
/**
* A handler that is called on input
*/
_onValueChanged: function() {
if (!this._elementReady) {
return;
}
this.fire('url-value-changed', {
value: this.value
});
this._handleAutoValidate();
},
// Displays the doc for the variable.
_displayVariableDoc: function(id) {
if (this.skipDocs) {
return;
}
var up = this.uriModel;
if (!id || !up) {
this.documentation = '';
return;
}
var item;
for (var i = 0, len = up.length; i < len; i++) {
if (up[i].name === id) {
item = up[i];
break;
}
}
if (!item || !item.description) {
this.documentation = '';
return;
}
this.documentation = item.description;
},
_onElementBlur: function() {
this.documentation = '';
},
/**
* A handler for the `url-value-changed` event.
* If this element is not the source of the event then it will update the `value` property.
* It's to be used besides the Polymer's data binding system.
*/
_extValueChangedHandler: function(e) {
if (e.target === this) {
return;
}
this.set('value', e.detail.value);
},
_getValidity: function() {
var value = this.value;
if (!this.required && !value) {
return true;
}
if (value === undefined) {
return true;
}
if (!value && this.required) {
return false;
}
if (!value) {
return true;
}
if (value.indexOf('{') !== -1 && value.indexOf('}') !== -1) {
return false;
}
// if ('URL' in window) {
// try {
// new URL(value);
// return true;
// } catch (e) {
// return false;
// }
// }
return this.$.input.validity.valid;
},
// Handler for input click / tap.
_inputTap: function() {
var variable = this._getSelectedVariable();
this._displayVariableDoc(variable);
},
/**
* Returns variable name for current caret position.
* This can be used for mouse on keyboard events to determine
* if current action involve a variable.
* It takes a selection start as a orientation point to find a variable.
*
* @return {String|undefined} Variable name or undefined if at current
* position there's no variable.
*/
_getSelectedVariable: function() {
var start = this.$.input.selectionStart;
var variable = this._computeVariableFromPosition(start);
return variable;
},
/**
* Computes variable from current caret position.
* It looks for brackets to the left and right from the selection and
* everything between is a variable name.
*
* @param {Number} start Caret position in thex field.
* @return {String|undefined} Variable name or undefined if at current
* position there's no variable.
*/
_computeVariableFromPosition: function(start) {
var value = this.value;
if (!start || !value) {
return;
}
// Check existance of the left bracket "{".
var pos;
var leftBracketPos = -1;
for (pos = start; pos > 0; pos--) {
if (value[pos] === '}') {
break;
}
if (value[pos] === '{') {
leftBracketPos = pos;
break;
}
}
if (leftBracketPos === -1) {
// Click is not in the vartiable.
return;
}
var rightBracketPos = -1;
for (pos = leftBracketPos; pos < value.length; pos++) {
var _val = value[pos];
if (_val === '}') {
rightBracketPos = pos;
break;
} else if (_val === '.' || _val === '/') {
break;
}
}
if (rightBracketPos === -1) {
return;
}
var variable = value.substr(leftBracketPos + 1,
rightBracketPos - leftBracketPos - 1);
return variable;
},
/**
* Creates a regular expression from the `_fullUri` to match the
* parameters in the `value` url.
*
* @param {String} url Enpoint's absolute URL with (possibly) parameters.
* @return {String} A RegExp that can be used to search for parameters values.
*/
_computeUrlRegexp: function(url) {
if (!url) {
return null;
}
url = url.replace(/(\.|\/)/g, '\\$1');
url = url.replace(/{\w+}/g, '([a-zA-Z0-9\\$\\-_\\.~\\+!\'\\(\\)\\*]+)');
return new RegExp(url);
},
/**
* Computes ordered list of parameters applied to the `_fullUri`.
* For example the URL: `http://{environment}.domain.com/{apiVersion}/`
*
* will be mapped to
* ```
* [
* "environment",
* "apiVersion"
* ]
* ```
*
* @param {String} url The URL to test for the parameters.
* @return {Array|null} An ordered list of parameters or null if none found.
*/
_computeUrlParams: function(url) {
if (!url) {
return null;
}
var paramsNames = url.match(/\{\w+\}/g);
if (paramsNames) {
paramsNames = paramsNames.map(function(item) {
return item.substr(1, item.length - 2);
});
}
return paramsNames;
},
/**
* Dispatches `quri/query-parameter-changed` custom event when a change in
* the model value has been detected.
*
* @param {[type]} record [description]
* @return {[type]} [description]
*/
_notifyModelChanged: function(record) {
if (!record) {
return;
}
if (record.path === 'queryModel' || record.path === 'uriModel') {
return;
}
if (record.path.indexOf('value') < 2) {
return;
}
var item = record.path.substr(0, record.path.lastIndexOf('.'));
if (!item) {
console.warn('Unable to notify parameter change. Unknown path', record.path);
return;
}
var type = record.path.substr(0, record.path.indexOf('.'));
var formType = type === 'queryModel' ? 'query' : 'uri';
this.fire(formType + '-parameter-changed', {
name: item.name,
value: item.value
});
}
});
</script>
</dom-module>