UNPKG

@postnord/web-components

Version:
527 lines (517 loc) 25.5 kB
/*! * 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