@postnord/web-components
Version:
PostNord Web Components
527 lines (517 loc) • 25.5 kB
JavaScript
/*!
* Built with Stencil
* By PostNord.
*/
import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client';
import { u as uuidv4, g as getBytesFromHumanReadableFileSize, k as awaitTopbar, o as ripple, e as en } from './helpers.js';
import { a as alert_exclamation_circle } from './alert_exclamation_circle.js';
import { c as check } from './check.js';
import { d as defineCustomElement$5 } from './pn-button2.js';
import { d as defineCustomElement$4 } from './pn-icon2.js';
import { d as defineCustomElement$3 } from './pn-progress-bar2.js';
import { d as defineCustomElement$2 } from './pn-spinner2.js';
const icon$2 = '<svg class="pn-icon-svg" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M9 7.5a4.5 4.5 0 1 1 9 0V16a6 6 0 0 1-12 0v-3a1 1 0 1 1 2 0v3a4 4 0 0 0 8 0V7.5a2.5 2.5 0 0 0-5 0V16a1 1 0 1 0 2 0v-6a1 1 0 1 1 2 0v6a3 3 0 1 1-6 0zM7 9a1 1 0 0 1 1 1v.1a1 1 0 1 1-2 0V10a1 1 0 0 1 1-1" clip-rule="evenodd"/></svg>';
const attachment = icon$2;
const icon$1 = '<svg class="pn-icon-svg" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M10.5 4a1 1 0 0 0-1 1h5a1 1 0 0 0-1-1zm6 1a3 3 0 0 0-3-3h-3a3 3 0 0 0-3 3h-2A2.5 2.5 0 0 0 5 9.95V19a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V9.95A2.5 2.5 0 0 0 18.5 5zm.5 5H7v9a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1zM5.5 7a.5.5 0 0 0 0 1h13a.5.5 0 0 0 0-1zm4.5 5a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0v-4a1 1 0 0 1 1-1m4 0a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0v-4a1 1 0 0 1 1-1" clip-rule="evenodd"/></svg>';
const trash_can = icon$1;
const icon = '<svg class="pn-icon-svg" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-2a1 1 0 1 1 0-2h2a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2a1 1 0 1 1 0 2H6a3 3 0 0 1-3-3zm9 0a1 1 0 0 1 .707.293l4 4a1 1 0 0 1-1.414 1.414L13 11.414V17a1 1 0 1 1-2 0v-5.586l-2.293 2.293a1 1 0 0 1-1.414-1.414l4-4A1 1 0 0 1 12 8m-1 12.1V20a1 1 0 1 1 2 0v.1a1 1 0 1 1-2 0" clip-rule="evenodd"/></svg>';
const upload = icon;
class PnFileUploadError extends Error {
element;
code;
data;
constructor(message, element, code = 'INVALID', data = null) {
super(message);
this.name = 'PNFileUploadError';
this.element = element;
this.code = code;
this.data = data;
}
}
const translations = {
CLICKORDRAG: {
sv: 'Klicka eller dra en fil hit',
en: 'Click or drag a file here',
da: 'Klik eller træk en fil hertil',
fi: 'Klikkaa tai vedä tiedosto tähän',
no: 'Klikk eller dra en fil hit',
},
ACCEPTEDFORMATS: {
sv: 'Filformat som stöds',
en: 'Supported formats',
da: 'Støttede filformater',
fi: 'Tuetut tiedostomuodot',
no: 'Understøttede filformater',
},
MAXFILESIZE: {
sv: 'Max filstorlek',
en: 'Max filesize',
da: 'Max filstørrelse',
fi: 'Maksimitiedostokoko',
no: 'Maks filstørrelse',
},
REMOVE_FILE: {
sv: 'Ta bort filen',
en: 'Remove file',
da: 'Fjern fil',
fi: 'Poista tiedosto',
no: 'Fjern fil',
},
};
const pnFileUploadCss = "pn-file-upload.hover .pn-file-upload-container .pn-dropzone-container{background-color:#e0f8ff}pn-file-upload .pn-file-upload-input{position:absolute;z-index:1;left:0;top:0;opacity:0;height:100%;width:100%;-webkit-tap-highlight-color:transparent}pn-file-upload .pn-file-upload-input:hover{cursor:pointer}pn-file-upload .pn-file-upload-input:hover+.pn-dropzone-inner{border-color:#005d92;background-color:#e0f8ff}pn-file-upload .pn-file-upload-input:focus-visible+.pn-dropzone-inner{border-color:#005d92;outline-color:#005d92}pn-file-upload .pn-file-upload-input:disabled{pointer-events:none}pn-file-upload .pn-file-upload-input:disabled+.pn-dropzone-inner{pointer-events:none;background-color:#f3f2f2;border-color:#2d2013}pn-file-upload .pn-file-upload-input:disabled+.pn-dropzone-inner .headline a{color:#5e554a}pn-file-upload .pn-file-upload-input:disabled+.pn-dropzone-inner .pn-icon-svg path{fill:#5e554a}pn-file-upload .pn-file-upload-container{position:relative;display:flex;flex-direction:column;gap:1em}pn-file-upload .pn-file-upload-container .pn-dropzone-container{position:relative}pn-file-upload .pn-file-upload-container .pn-dropzone-inner{position:relative;text-align:center;overflow:hidden;padding:2.5em;border:0.0625em #969087 dashed;border-radius:0.5em;display:flex;flex-direction:column;align-items:center;gap:0.5em;outline:0.2rem solid transparent;outline-offset:0.2rem;transition-property:background-color, outline-color, border-color;transition-duration:0.2s;transition-timing-function:cubic-bezier(0.7, 0, 0.3, 1)}@media (prefers-reduced-motion: reduce){pn-file-upload .pn-file-upload-container .pn-dropzone-inner{transition-duration:0s;transition-delay:0s}}pn-file-upload .pn-file-upload-container .pn-dropzone-inner .pn-ripple{animation:ripple 0.4s cubic-bezier(0.7, 0, 0.3, 1);position:absolute;border-radius:50%;background-color:#005d92;transform:translate(-50%, -50%) scale(0);opacity:0.1;pointer-events:none;z-index:3}@keyframes ripple{to{transform:translate(-50%, -50%) scale(1);opacity:0}}pn-file-upload .pn-file-upload-container .pn-dropzone-inner[data-hover]{background-color:#e0f8ff;border-color:#005d92}pn-file-upload .pn-file-upload-container .pn-dropzone-inner[data-invalid]{border-color:#a70707}pn-file-upload .pn-file-upload-container .pn-dropzone-inner .pn-file-upload-dropzone{display:flex;flex-direction:row;align-items:center;justify-content:center;gap:0.5em}pn-file-upload .files-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1em}pn-file-upload .files-list .filename{font-weight:400;font-size:0.75em}pn-file-upload .files-list .files-list-item{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;align-items:center;background:#f9f8f8;padding:1em;border-radius:0.5em}pn-file-upload .files-list .files-list-item .file-list-item-title{display:flex;align-items:center;gap:0.5em;margin-right:0.5em}pn-file-upload .files-list .files-list-item>pn-progress-bar{margin-right:25em;flex:1}pn-file-upload .files-list .files-list-item.files-list-item-error{color:#a70707;background-color:#fdefee}pn-file-upload .files-list .files-list-item-text-error{color:#a70707;font-size:0.875em;font-weight:300;margin-top:0.25em}pn-file-upload .pn-file-upload-title{color:#2d2013}pn-file-upload .pn-file-upload-subtitle{color:#5e554a;font-size:0.75em}";
const PnFileUpload$1 = /*@__PURE__*/ proxyCustomElement(class PnFileUpload extends HTMLElement {
constructor(registerHost) {
super();
if (registerHost !== false) {
this.__registerHost();
}
this.filesAdded = createEvent(this, "filesAdded", 7);
this.update = createEvent(this, "update", 7);
this.fileuploaderror = createEvent(this, "fileuploaderror", 7);
this.valid = createEvent(this, "valid", 7);
this.uploadFile = createEvent(this, "uploadFile", 7);
this.uploadCancelled = createEvent(this, "uploadCancelled", 7);
this.uploadCompleted = createEvent(this, "uploadCompleted", 7);
}
id = `pn-file-upload-${uuidv4()}`;
idFileUpload = `${this.id}-label`;
idHelpertext = `${this.id}-helpertext`;
idAccepttext = `${this.id}-accept`;
fileInputElement;
invalid = false;
maxSizeInBytes = 0;
get hostElement() { return this; }
files = [];
isHovering = false;
/** The default label is "Click or drag a file here". @since v7.1.0 */
label;
/**
* The helpertext defaults to "Supported formats {x} and max filesize {y}",
* if there is a value set for the prop `accept`.
* @since v7.1.0
**/
helpertext;
/** Manually set the language. @category Features */
language = null;
/** Always hide the progress bar, even when upload has begun. @category Features */
hideProgress = false;
/**
* Set a HTML name.
* @since v7.1.0
* @category HTML attributes
*/
name;
/** Set which file types the input accepts. Ex: .docs,.xml @category HTML attributes */
accept;
/** Set the maximum upload size. @category HTML attributes */
maxSize = '';
/**
* Use the capture mode.
* @since v7.1.0
* @category HTML attributes
*/
capture;
/**
* Set maximum amount of files to be selected.
* Any value above 1 will force the `multiple` to be `true`.
* @category HTML attributes
**/
limit = 1;
/** Allow multiple files to be selected. Will be overwritten to true, if `limit` is above 1. @category HTML attributes */
multiple;
/** Set the input as required. @category HTML attributes */
required = false;
/** Disable the input. @category HTML attributes */
disabled = false;
handleLimit() {
this.multiple = this.limit > 1;
}
handleMaxBytes() {
this.maxSizeInBytes = getBytesFromHumanReadableFileSize(this.maxSize);
}
/** This event is emitted when you add files. Either via drag and drop or using the file explorer. */
filesAdded;
/** Emitted everytime an update is made to one of the files. */
update;
/** If any error occurs, such as: invalid file type, size, limit or error during upload. */
fileuploaderror;
/** Emitted everytime you select a valid file. It's a good idea to use this event in order to make use of the built in validation features. */
valid;
/** Emitted once the file upload has begun. */
uploadFile;
/** Emitted when an upload has been cancelled before completion. */
uploadCancelled;
/** Emitted once the upload is finished. */
uploadCompleted;
async componentWillLoad() {
this.handleLimit();
this.handleMaxBytes();
if (this.language === null)
await awaitTopbar(this.hostElement);
}
/** Start the upload of all selected files (excludes invalid files). */
async startUpload() {
if (!this.files.length)
return;
this.files = this.files.map((file, index) => {
if (file.status === 'queue') {
file.status = 'start';
this.uploadFile.emit({
file: file,
index: index,
uploader: this,
});
}
return file;
});
const promises = this.files.map(file => file.xhrPromise().catch(() => { }));
return Promise.all(promises).then(data => {
const added = data.filter(item => item);
this.uploadCompleted.emit(added);
});
}
/** Clear the selected files, state and input value. */
async clearUpload() {
this.files = [];
this.fileInputElement.value = '';
this.invalid = false;
this.filesAdded.emit([]);
}
/** Remove the selected file. */
async removeFile(targetFile) {
let valid = true;
const files = [];
for (const fileIndex in this.files) {
const file = this.files[fileIndex];
if (!file.filename) {
continue;
}
// add to files if the uuid doesn't match
if (file.uuid !== targetFile.uuid) {
try {
this._validateFileSize(file);
this._validateFileType(file);
valid = true;
}
catch (error) {
valid = false;
file.error = error;
file.progress = 0;
file.status = 'error';
}
files.push(file);
}
else if (file.status === 'start') {
file.xhr.abort();
this.uploadCancelled.emit(file);
}
}
this.filesAdded.emit(files);
this.files = files;
this.invalid = !valid || !this._isNotGreaterThanLimit();
this._checkValidity();
this._emitUpdateEvent();
}
/** Update an existing file. */
async updateFile(file, index = null) {
let files = this.files;
// find the index if not defined
if (index === null) {
for (const fIndex in files) {
const fFile = files[fIndex];
if (fFile.uuid === file.uuid) {
index = fIndex;
break;
}
}
}
if (files[index]) {
if (file.progress >= 100) {
file.progress = 100;
file.status = 'completed';
}
else if (file.progress < 0) {
file.status = 'start';
file.progress = 0;
}
if (file.error) {
file.progress = 0;
file.status = 'error';
file.errorMessage = file.errorMessage || 'There was an error uploading your file, please try again';
}
files[index] = file;
}
this.files = [...files];
}
async _addFilesFromFileList(fileList) {
if (this.disabled || this.invalid) {
return;
}
const files = Array.from(fileList);
const promises = [];
if (this.multiple) {
files.forEach(file => promises.push(this._addFile(file)));
}
else {
const file = files[0];
if (this.limit === 1) {
this.files = [];
promises.push(this._addFile(file));
}
else {
promises.push(this._addFile(file));
}
}
return Promise.all(promises).then(() => {
this.filesAdded.emit(this.files);
this._isNotGreaterThanLimit();
this._checkValidity();
this._emitUpdateEvent();
});
}
async _onAddFile() {
const files = this.fileInputElement.files;
return await this._addFilesFromFileList(files);
}
_onDragOverFile(event) {
event.preventDefault();
event.stopPropagation();
this.isHovering = true;
}
_onDragLeaveFile(event) {
event.preventDefault();
event.stopPropagation();
this.isHovering = false;
}
_onDropFile(event) {
event.preventDefault();
event.stopPropagation();
this.isHovering = false;
ripple(event, this.hostElement, '.pn-dropzone-inner');
this._addFilesFromFileList(event.dataTransfer.files);
}
_validateFileType(file) {
const accepts = this.accept ? this.accept.split(',') : [];
if (accepts.length) {
let valid = false;
for (const accept of accepts) {
const type = accept.trim().toLowerCase();
const isMimeType = /^\w+\/[a-z\.\-]+$/i;
const extension = file.filename.match(/\.\w{3,4}($|\?)/)?.[0]?.toLowerCase();
if ((isMimeType.test(type) && file.contentType === type) || extension === type) {
valid = true;
break;
}
}
if (!valid) {
const error = new PnFileUploadError(`Invalid file type.`, this.hostElement, 'INVALID_FILE_TYPE', file);
this.invalid = true;
this.fileuploaderror.emit(error);
throw error;
}
}
}
_validateFileSize(file) {
if (this.maxSizeInBytes > 0 && file.filesize > this.maxSizeInBytes) {
const error = new PnFileUploadError(`You can only add up to ${this.maxSize}.`, this.hostElement, 'MAX_FILE_SIZE', file);
this.invalid = true;
this.fileuploaderror.emit(error);
throw error;
}
}
_validateLimit() {
if (this.limit && this.files.length > this.limit) {
const error = new PnFileUploadError(`You can only add up to ${this.limit} files.`, this.hostElement, 'MAX_LIMIT', this.files);
this.invalid = true;
this.fileuploaderror.emit(error);
throw error;
}
}
_isNotGreaterThanLimit() {
try {
this._validateLimit();
return true;
}
catch (error) {
return false;
}
}
_checkValidity() {
if (!this.invalid) {
this.valid.emit(this);
}
return !this.invalid;
}
_createBase64FromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
reader.onabort = () => reject('ABORTED');
reader.readAsDataURL(file);
});
}
async _addFile(file) {
return this._createBase64FromFile(file).then(data => {
const split = data.split(',');
const base64 = split[1];
const contentType = split[0].split(':')[1].split(';')[0];
const request = new XMLHttpRequest();
const uploadFile = {
uuid: uuidv4(),
filename: file.name,
filesize: file.size,
progress: 0,
status: 'queue',
base64,
contentType,
error: null,
errorMessage: '',
file,
xhr: request,
};
try {
this._validateFileSize(uploadFile);
this._validateFileType(uploadFile);
}
catch (error) {
uploadFile.error = error;
uploadFile.progress = 0;
uploadFile.status = 'error';
}
uploadFile.xhrPromise = () => {
return new Promise((resolve, reject) => {
request.responseType = 'json';
request.addEventListener('progress', event => {
if (event.lengthComputable) {
uploadFile.progress = (event.loaded / event.total) * 100;
this.updateFile(uploadFile);
}
else {
uploadFile.progress = 100;
this.updateFile(uploadFile);
}
});
request.addEventListener('load', (response) => resolve(response.target.response));
request.addEventListener('error', error => {
uploadFile.error = error;
uploadFile.errorMessage = 'There was an error uploading your file, please try again';
this.updateFile(uploadFile);
const errorEvent = new PnFileUploadError('There was an error uploading your file, please try again', this.hostElement, 'UPLOAD_SERVER_ERROR', uploadFile);
this.fileuploaderror.emit(errorEvent);
reject(error);
});
});
};
this.files = [...this.files, uploadFile];
return uploadFile;
});
}
_emitUpdateEvent() {
this.update.emit({
files: this.files,
element: this.hostElement,
});
}
translate(prop) {
return translations?.[prop]?.[this.language || en] || prop;
}
isLoading(file) {
return file.status === 'start';
}
getFileItemClass = (file) => {
let itemClass = 'files-list-item';
if (file.error) {
itemClass += ' files-list-item-error';
}
return itemClass;
};
getFileIcon(file) {
if (file.error)
return alert_exclamation_circle;
if (file.progress === 100)
return check;
return attachment;
}
getFileColor(file) {
if (file.error)
return 'warning';
if (file.progress === 100)
return 'green700';
return 'gray900';
}
describedBy() {
const list = [this.accept && this.idAccepttext, this.helpertext && this.idHelpertext].filter(Boolean);
if (!list.length)
return null;
return list.join(',');
}
render() {
return (h(Host, { key: '9f4cd4a2ff2fdeafb3453f6d21b238d621bbf757' }, h("div", { key: 'd47ca591a3459580431447aa350ef0f08da43166', class: "pn-file-upload-container" }, h("div", { key: '0aee23a11b81461133a8035cd682f1cebaf329e1', class: "pn-dropzone-container" }, h("input", { key: '7741295384c4343454e33adc6194f0d7fba27d4e', id: this.idFileUpload, class: "pn-file-upload-input", type: "file", name: this.name, multiple: this.multiple, accept: this.accept, capture: this.capture, disabled: this.disabled, required: this.required, "aria-describedby": this.describedBy(), "aria-invalid": this.invalid?.toString(), onClick: e => ripple(e, this.hostElement, '.pn-dropzone-inner'), onChange: () => this._onAddFile(), onDragOver: e => this._onDragOverFile(e), onDragLeave: e => this._onDragLeaveFile(e), onDrop: e => this._onDropFile(e), ref: el => (this.fileInputElement = el) }), h("div", { key: '5797bf8e22056be5eadf4d240a2dc901519dbe36', class: "pn-dropzone-inner", "data-hover": this.isHovering, "data-invalid": this.invalid }, h("pn-icon", { key: '76deb10dfe5f311433673a8f2d6defd8ca128cf8', class: "pn-file-upload-icon-element", icon: upload, color: "blue700" }), h("label", { key: '01daf31ff4a6b77036050a1ee6ccc4db9b57a614', htmlFor: this.idFileUpload, class: "pn-file-upload-title" }, this.label || this.translate('CLICKORDRAG')), this.helpertext && (h("p", { key: '6db4ade5ceeb16a8c197a649de88b0fc228141d6', id: this.idHelpertext, class: "pn-file-upload-subtitle" }, this.helpertext)), this.accept && (h("p", { key: '9de51566ceeb314f005e7cb3f1e581da64ffa6d0', id: this.idAccepttext, class: "pn-file-upload-subtitle" }, this.translate('ACCEPTEDFORMATS'), ": ", this.accept, this.maxSize && (h("span", { key: '2f9dfeda80c70b84b04698351c8c33d5724954b6' }, ' ', "(", this.translate('MAXFILESIZE'), ": ", this.maxSize, ")")))))), !!this.files.length && (h("ul", { key: 'dd790d2f9a9712450e0f9dd67cd92e548c29fe34', class: "files-list" }, this.files.map((file) => (h("li", { class: "files-list-item-container", key: file.uuid }, h("div", { class: this.getFileItemClass(file) }, h("div", { class: "file-list-item-title" }, h("pn-icon", { icon: this.getFileIcon(file), color: this.getFileColor(file) }), this.hideProgress || file.progress === 0 ? (h("div", { class: "filename" }, file.filename)) : (h("pn-progress-bar", { label: file.filename, progress: file.progress, error: file.error && file.errorMessage }))), h("pn-button", { small: true, variant: "borderless", arialabel: `${this.translate('REMOVE_FILE')} ${file.filename}`, appearance: file.error ? 'warning' : 'light', icon: trash_can, iconOnly: true, tooltip: this.translate('REMOVE_FILE'), loading: this.isLoading(file), onClick: () => this.removeFile(file) })), file.error ? (h("div", { class: "files-list-item-text-error" }, file.errorMessage || file.error.message)) : null))))), h("slot", { key: 'd32f3097b4162d4a67efab738903acbda6937929' }))));
}
static get watchers() { return {
"limit": ["handleLimit"],
"maxSize": ["handleMaxBytes"]
}; }
static get style() { return pnFileUploadCss; }
}, [260, "pn-file-upload", {
"label": [1],
"helpertext": [1],
"language": [1],
"hideProgress": [4, "hide-progress"],
"name": [1],
"accept": [1],
"maxSize": [1, "max-size"],
"capture": [1],
"limit": [2],
"multiple": [1028],
"required": [4],
"disabled": [4],
"files": [32],
"isHovering": [32],
"startUpload": [64],
"clearUpload": [64],
"removeFile": [64],
"updateFile": [64]
}, undefined, {
"limit": ["handleLimit"],
"maxSize": ["handleMaxBytes"]
}]);
function defineCustomElement$1() {
if (typeof customElements === "undefined") {
return;
}
const components = ["pn-file-upload", "pn-button", "pn-icon", "pn-progress-bar", "pn-spinner"];
components.forEach(tagName => { switch (tagName) {
case "pn-file-upload":
if (!customElements.get(tagName)) {
customElements.define(tagName, PnFileUpload$1);
}
break;
case "pn-button":
if (!customElements.get(tagName)) {
defineCustomElement$5();
}
break;
case "pn-icon":
if (!customElements.get(tagName)) {
defineCustomElement$4();
}
break;
case "pn-progress-bar":
if (!customElements.get(tagName)) {
defineCustomElement$3();
}
break;
case "pn-spinner":
if (!customElements.get(tagName)) {
defineCustomElement$2();
}
break;
} });
}
const PnFileUpload = PnFileUpload$1;
const defineCustomElement = defineCustomElement$1;
export { PnFileUpload, defineCustomElement };
//# sourceMappingURL=pn-file-upload.js.map
//# sourceMappingURL=pn-file-upload.js.map