@lrnwebcomponents/eco-json-schema-form
Version:
JSON Schema form data binding magic
689 lines (626 loc) • 17.2 kB
JavaScript
import { html, PolymerElement } from "@polymer/polymer/polymer-element.js";
import { microTask } from "@polymer/polymer/lib/utils/async.js";
import "@polymer/iron-flex-layout/iron-flex-layout-classes.js";
import { AppLocalizeBehavior } from "@polymer/app-localize-behavior/app-localize-behavior.js";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class.js";
import "@polymer/paper-input/paper-input.js";
import "@lrnwebcomponents/simple-icon/simple-icon.js";
import "@lrnwebcomponents/simple-icon/lib/simple-icons.js";
import "@lrnwebcomponents/simple-progress/simple-progress.js";
import "@polymer/polymer/lib/elements/dom-repeat.js";
/**
`eco-json-schema-file` takes in a JSON schema of type number and string and
contains a `paper-input`, exposing a `value` property that represents the schema.
Validation is handled for strings and number/integers by mapping JSON schema
validation keywords to `paper-input` attributes; form elements will automatically
try and validate themselves as users provide input:
Please see the `eco-json-schema-object` documentation for further information.
@group eco Elements
@element eco-json-schema-file
* @demo demo/index.html
*/
class EcoJsonSchemaFile extends mixinBehaviors(
[AppLocalizeBehavior],
PolymerElement
) {
static get tag() {
return "eco-json-schema-file";
}
static get template() {
return html`
<style is="custom-style" include="iron-flex iron-flex-alignment">
:host ([hidden]) {
display: none;
}
paper-input {
padding: 2px;
--paper-input-container-label: {
white-space: normal;
position: static;
font-size: 22px;
color: #212121;
}
}
:host {
display: inline-block;
}
.enabled {
border: 1px dashed #555;
@apply --file-upload-upload-border-enabled;
}
.hover {
opacity: 0.7;
border: 1px dashed #111;
@apply --file-upload-upload-border-hover;
}
#UploadBorder {
vertical-align: middle;
color: #555;
padding: 20px;
max-height: 300px;
overflow-y: auto;
display: inline-block;
@apply --file-upload-upload-border;
}
#dropArea {
text-align: center;
@apply --file-upload-drop-area;
}
button#button {
margin-bottom: 20px;
@apply --file-upload-button;
}
.file {
padding: 10px 0px;
@apply --file-upload-file;
}
.commands {
float: right;
@apply --file-upload-commands;
}
.commands iron-icon:not([icon="check-circle"]) {
cursor: pointer;
opacity: 0.9;
@apply --file-upload-commands-faded;
}
.commands iron-icon:hover {
opacity: 1;
@apply --file-upload-commands-hovered;
}
[hidden] {
display: none;
}
.error {
color: #f40303;
font-size: 11px;
margin-top: 2px;
@apply --file-upload-error;
}
.progress-bar {
margin-top: 2px;
}
simple-progress {
--simple-progress-color: #03a9f4;
}
simple-progress[error] {
--simple-progress-color: #f40303;
}
</style>
<div class="layout horizontal nowrap">
<div>
<button
id="button"
on-click="_fileClick"
alt="{{paperButtonAlt}}"
raised=""
>
<simple-icon icon="editor:attach-file"></simple-icon
>{{paperButtonTitle}}
</button>
<div id="UploadBorder">
<div id="dropArea" hidden$="{{!_shownDropText}}">{{dropText}}</div>
<template is="dom-repeat" items="{{files}}">
<div class="file">
<div class="name">
<span>{{item.name}}</span>
<div class="commands">
<simple-icon
icon="autorenew"
title="{{retryText}}"
on-click="_retryUpload"
hidden$="{{!item.error}}"
></simple-icon>
<simple-icon
icon="cancel"
title="{{removeText}}"
on-click="_cancelUpload"
hidden$="{{item.complete}}"
></simple-icon>
<simple-icon
icon="check-circle"
title="{{successText}}"
hidden$="{{!item.complete}}"
></simple-icon>
</div>
</div>
<div class="error" hidden$="{{!item.error}}">{{errorText}}</div>
</div>
</template>
</div>
</div>
<input
type="file"
id="fileInput"
on-change="_fileChange"
hidden=""
multiple="{{multi}}"
accept="{{accept}}"
/>
</div>
`;
}
static get properties() {
return {
language: {
value: "en",
notify: true,
},
resources: {
value() {
return {};
},
notify: true,
},
schema: {
type: Object,
observer: "_schemaChanged",
},
value: {
type: Object,
notify: true,
value() {
return {};
},
observer: "_valueChanged",
},
/**
error: {
type: String,
observer: '_errorChanged',
value: null
},
*/
/**
* `target`
* @element target is the target url to upload the files to.
* Additionally by adding '<name>' in your url, it will be replaced by
* the file name.
*/
target: {
type: String,
value: "",
},
/**
* `accept`
* @element accept is the set of comma separated file extensions or mime types
* to filter as accepted.
*/
accept: {
type: String,
value: "",
},
/**
* `droppable`
* @element droppable indicates whether or not to allow file drop.
*/
droppable: {
type: Boolean,
value: false,
},
/**
* `dropText`
* @element dropText is the text to display in the file drop area.
*/
dropText: {
type: String,
value: "Drop Files Here",
},
/**
* `multi`
* @element multi indicates whether or not to allow multiple files to be uploaded.
*/
multi: {
type: Boolean,
value: true,
},
/**
* `files`
* @element files is the list of files to be uploaded
*/
files: {
type: Array,
notify: true,
value() {
return [];
},
},
/**
* `raised`
* @element raised indicates whether or not the button should be raised
*/
raised: {
type: Boolean,
value: true,
},
/**
* `noink`
* @element noink indicates that the button should not have an ink effect
*/
noink: {
type: Boolean,
value: false,
},
/**
* `headers`
* @element headers is a key value map of header names and values
*/
headers: {
type: Object,
value: {},
},
/**
* `retryText`
* @element retryText is the text for the tooltip to retry an upload
*/
retryText: {
type: String,
value: "Retry Upload",
},
/**
* `removeText`
* @element removeText is the text for the tooltip to remove an upload
*/
removeText: {
type: String,
value: "Remove",
},
/**
* `successText`
* @element successText is the text for the tooltip of a successful upload
*/
successText: {
type: String,
value: "Success",
},
/**
* `errorText`
* @element errorText is the text to display for a failed upload
*/
errorText: {
type: String,
value: "Error uploading file...",
},
/**
* `_shownDropText`
* @element _shownDropText indicates whether or not the drop text should be shown
*/
_shownDropText: {
type: Boolean,
value: false,
},
/**
* `additional`
* @element additional object of key-pair values to send additional values along with file.
*/
additional: {
type: Object,
value: {},
},
/**
* `fileDataName`
* @element fileDataName is the name for the file data in the `formData` object.
*/
fileDataName: {
type: String,
value: "file",
},
/**
* `paperButtonAlt`
* @element paperButtonAlt allows changing the alt property on the paper button
*/
paperButtonAlt: {
type: String,
value: "",
},
/**
* `paperButtonTitle`
* @element paperButtonTitle allows changing the title property on the paper button
*/
paperButtonTitle: {
type: String,
value: "",
},
};
}
/**
* Clears the list of files
*/
clear() {
this.set("files", []);
this.shadowRoot.querySelector("#fileInput").value = "";
this._showDropText();
}
ready() {
super.ready();
if (this.raised) {
this.toggleAttribute(
"raised",
true,
this.shadowRoot.querySelector("#button")
);
}
if (this.noink) {
this.toggleAttribute(
"noink",
true,
this.shadowRoot.querySelector("#button")
);
}
if (this.droppable) {
this._showDropText();
this.setupDrop();
}
}
/**
* A function to set up a drop area for drag-and-drop file uploads
*/
setupDrop() {
var uploadBorder = this.shadowRoot.querySelector("#UploadBorder");
this.toggleClass("enabled", true, uploadBorder);
this.ondragover = function (e) {
e.stopPropagation();
this.toggleClass("hover", true, uploadBorder);
// Workaround for allowgin drop from Chome's download footer on OSX
// See https://bugs.chromium.org/p/chromium/issues/detail?id=234931
var effect = e.dataTransfer && e.dataTransfer.dropEffect;
var effectAllowed = e.dataTransfer && e.dataTransfer.effectAllowed;
if (effect === "none" && effectAllowed !== "none") {
e.dataTransfer.dropEffect = effectAllowed === "move" ? "move" : "copy";
}
// end of workaround
return false;
};
this.ondragleave = function () {
this.toggleClass("hover", false, uploadBorder);
return false;
};
this.ondrop = function (event) {
this.toggleClass("hover", false, uploadBorder);
event.preventDefault();
// Check if multiple upload is allowed
if (!this.multi && this.files.length !== 0) {
return;
}
var length = event.dataTransfer.files.length;
for (var i = 0; i < length; i++) {
var file = event.dataTransfer.files[i];
//if (this.value.indexOf(window.btoa(unescape(encodeURIComponent(file.name)))) >= 0 ) {
// continue;
//}
// Check if filetype is accepted
var mimeType =
file.type !== "" ? file.type.match(/^[^\/]*\//)[0] : null;
var fileType = file.name.match(/\.[^\.]*$/)[0];
if (
this.accept !== "" &&
!(
this.accept.indexOf(mimeType) > -1 ||
this.accept.indexOf(fileType) > -1
)
) {
continue;
}
file.progress = 0;
file.error = false;
file.complete = false;
this.push("files", file);
this.uploadFile(file);
}
};
}
/**
* Clicks the invisible file input
*/
_fileClick() {
var elem = this.shadowRoot.querySelector("#fileInput");
if (elem && document.createEvent) {
// sanity check
var evt = document.createEvent("MouseEvents");
evt.initEvent("click", true, false);
elem.dispatchEvent(evt);
}
}
/**
* Called whenever the list of selected files changes
*
* @param {object} e An event object
*/
_fileChange(e) {
var length = e.target.files.length;
for (var i = 0; i < length; i++) {
var file = e.target.files[i];
file.progress = 0;
file.error = false;
file.complete = false;
this.push("files", file);
if (!this.multi && this.files.length !== 0) {
this.set("files", []);
this.set("value", {});
}
this.uploadFile(file);
}
}
/**
* Cancels the file upload for a specific file
*
* @param {object} file An element of the files array
*/
cancel(file) {
if (file) {
if (file.xhr) {
file.xhr.abort();
}
this.splice("files", this.files.indexOf(file), 1);
this._showDropText();
}
}
/**
* Cancels the file upload
*
* @param {object} e An event object
*/
_cancelUpload(e) {
this.cancel(e.model.__data__.item);
}
/**
* Retries to upload the file
*
* @param {object} e An event object
*/
_retryUpload(e) {
e.model.set("item.error", false);
e.model.set("item.progress", 0);
// The async helps give visual feedback of a retry occurring, even though it's less efficient.
var self = this;
microTask.run(() => {
self.uploadFile(e.model.__data__.item);
});
}
/**
* Whether or not to display the drop text
*/
_showDropText() {
this.set("_shownDropText", !this.files.length && this.droppable);
}
/**
* Uploads a file
*
* @param {object} file An element of the files array
*/
uploadFile(file) {
if (!file) {
return;
}
this.dispatchEvent(
new CustomEvent("before-upload", {
bubbles: true,
cancelable: true,
composed: true,
detail: true,
})
);
this._showDropText();
var prefix = "files." + this.files.indexOf(file);
var self = this;
var reader = new FileReader();
reader.addEventListener(
"load",
function () {
var r = reader.result;
//self.push('value', {"name": unescape(encodeURIComponent( file.name )),"content":r});
//self.value.push({"name": unescape(encodeURIComponent( file.name )),"content":r});
//console.log('value.'+self.files.indexOf(file));
self.set(
"value." +
self.attributes.name.value +
"." +
self.files.indexOf(file),
r
);
//self.notifyPath('value');
},
false
);
if (!self.value.hasOwnProperty(self.attributes.name.value)) {
this.set("value." + self.attributes.name.value, {});
}
reader.readAsDataURL(file);
}
_valueChanged() {
console.log("this.value: " + JSON.stringify(this.value));
}
_schemaChanged() {
var schema = this.schema;
/*
var inputEl = this.shadowRoot.querySelector('#fileInput');
if (schema.required) {
inputEl.required = true;
}
//inputEl.type = 'file';
if (schema.component && schema.component.properties) {
Object.keys(schema.component.properties).forEach(function(prop) {
inputEl[prop] = schema.component.properties[prop];
});
}
*/
/*
inputEl.alwaysFloatLabel = true; // label doesn't float when value not set
if (schema.title) {
inputEl.label = schema.title;
}
*/
}
/*
_errorChanged() {
if (this.error) {
this.shadowRoot.querySelector('#fileInput').errorMessage = this.error;
this.shadowRoot.querySelector('#fileInput').invalid = true;
} else {
this.shadowRoot.querySelector('#fileInput').invalid = false;
this.shadowRoot.querySelector('#fileInput').errorMessage = null;
}
},
*/
_isSchemaValue(type) {
return this._isSchemaFile(type);
}
_isSchemaFile(type) {
if (Array.isArray(type)) {
return type.indexOf("file") !== -1;
} else {
return type === "file";
}
}
_isSchemaBoolean(type) {
if (Array.isArray(type)) {
return type.indexOf("boolean") !== -1;
} else {
return type === "boolean";
}
}
_isSchemaNumber(type) {
if (Array.isArray(type)) {
return type.indexOf("number") !== -1 || type.indexOf("integer") !== -1;
} else {
return type === "number" || type === "integer";
}
}
_isSchemaString(type) {
if (Array.isArray(type)) {
return type.indexOf("string") !== -1;
} else {
return type === "string";
}
}
_isSchemaObject(type) {
return type === "object";
}
_isSchemaArray(type) {
return type === "array";
}
stringify(s) {
return JSON.stringify(s);
}
}
customElements.define(EcoJsonSchemaFile.tag, EcoJsonSchemaFile);
export { EcoJsonSchemaFile };