UNPKG

wj-elements

Version:

WebJET Elements is a modern set of user interface tools harnessing the power of web components designed to simplify web application development.

553 lines (552 loc) 18.2 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Localizer } from "./localize.js"; import Button from "./wje-button.js"; import WJElement from "./wje-element.js"; function fileType() { return [ { type: ["jpg", "jpeg", "png", "gif", "bpm", "tiff", "svg"], name: "photo" }, { type: ["zip", "rar", "cab", "jar", "tar", "gzip", "uue", "bz2", "scorm", "war"], name: "file-type-zip" }, { type: ["mov", "mp4", "avi", "flv"], name: "video" }, { type: ["m4a", "mp3", "wav"], name: "audio" }, { type: ["html", "html"], name: "file-type-html" }, { type: ["css"], name: "code" }, { type: ["txt"], name: "file-type-txt" }, { type: ["doc", "docx"], name: "file-type-doc" }, { type: ["xls", "xlsx"], name: "file-type-xls" }, { type: ["pdf"], name: "file-type-pdf" }, { type: ["ppt", "pptx", "odp"], name: "file-type-ppt" } ]; } function getFileTypeIcon(type) { let searchType; if (type.toLowerCase() !== "folder") { fileType().forEach((i) => { if (i.type.includes(type.toLowerCase())) { searchType = i.name; } }); } else { searchType = "folder"; } return searchType; } function isValidFileType(file, acceptedFileTypes) { const baseMimeType = file.type.split("/")[0]; let acceptedTypes = Array.isArray(acceptedFileTypes) ? acceptedFileTypes : acceptedFileTypes.split(","); if (acceptedTypes.length === 0) { throw new Error("acceptedFileTypes is empty"); } for (let type of acceptedTypes) { if (type.includes(baseMimeType + "/*")) { return true; } if (type.includes(file.type) || type.includes(file.type.split("/")[1])) { return true; } } return false; } function upload(url, chunkSize = 1024 * 1024, wholeFile = false) { if (wholeFile) { return (file, preview) => uploadWholeFile(url, file, preview); } return (file, preview) => uploadFileInChunks(url, file, preview, chunkSize); } async function uploadFileInChunks(url, file, preview, chunkSize = 1024 * 1024) { let offset = 0; const totalChunks = Math.ceil(file.size / chunkSize); const partResponses = []; while (offset < file.size) { const chunk = file.slice(offset, offset + chunkSize); const stream = new ReadableStream({ start(controller) { const reader = chunk.stream().getReader(); let uploadedBytes = 0; reader.read().then(function process({ done, value }) { if (done) { controller.close(); return Promise.resolve(); } uploadedBytes += value.byteLength; const percentComplete = (offset + uploadedBytes) / file.size * 100; console.log(`Upload Progress: ${percentComplete.toFixed(2)}%`); preview.setAttribute("uploaded", offset + uploadedBytes); controller.enqueue(value); return reader.read().then(process); }); } }); const formData = new FormData(); formData.append("file", new Blob([stream])); formData.append("chunkIndex", Math.floor(offset / chunkSize)); formData.append("totalChunks", totalChunks); formData.append("fileName", file.name); try { const response2 = await fetch(url, { method: "POST", body: formData }); if (!response2.ok) { throw new Error(`Failed to upload chunk ${Math.floor(offset / chunkSize) + 1}: ${response2.statusText}`); } console.log(`Chunk ${Math.floor(offset / chunkSize) + 1}/${totalChunks} uploaded successfully.`); partResponses.push(response2); } catch (error) { console.error("Error uploading chunk:", error); break; } offset += chunkSize; } console.log("File upload complete!"); const response = await partResponses.at(-1).json(); return { data: response, file }; } function uploadWholeFile(url, file, preview) { const formData = new FormData(); formData.append("file", file); return fetch(url, { method: "POST", body: formData }).then((response) => response.json()).then((data) => { preview.setAttribute("uploaded", file.size); return { data, file }; }).catch((error) => { console.error("Error:", error); }); } const styles = "/*\n[ WJ File Upload ]\n*/\n\n:host {\n width: 100%;\n}\n\n.native-file-upload {\n width: 100%;\n position: relative;\n border: 1px dashed var(--wje-border-color);\n border-radius: var(--wje-border-radius-medium);\n}\n\n.file-label {\n background: var(--wje-color-contrast-0);\n align-items: center;\n justify-content: center;\n display: flex;\n padding: 1rem;\n margin-bottom: 0.5rem;\n flex-direction: column;\n position: relative;\n}\n\n.file-preview {\n display: grid;\n grid-template-columns: auto 1fr 1fr;\n grid-template-rows: auto auto auto;\n gap: 0 0;\n grid-template-areas:\n 'image name name'\n 'image size size'\n 'progress progress progress';\n}\n\n.file-image {\n grid-area: image;\n align-items: center;\n display: flex;\n}\n\n.file-name {\n grid-area: name;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.file-size {\n grid-area: size;\n display: flex;\n}\n\n.file-progress {\n grid-area: progress;\n}\n\nwje-icon {\n margin-right: 0.25rem;\n}\n\nwje-img {\n margin-right: 0.25rem;\n}\n\n.file-info > span {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\nwje-slider {\n flex-basis: 100%;\n margin-top: 0.5rem;\n}\n\n::part(slider) {\n &::-webkit-slider-thumb {\n visibility: hidden;\n }\n\n &::-moz-range-thumb {\n visibility: hidden;\n }\n\n &::-ms-thumb {\n visibility: hidden;\n }\n}\n\nwje-img {\n width: 50px;\n height: 50px;\n display: flex;\n align-items: center;\n padding: 0.25rem;\n border: 1px solid var(--wje-border-color);\n border-radius: var(--wje-border-radius-medium);\n}\n"; class FileUpload extends WJElement { /** * Constructor for FileUpload. * Initializes a new instance of the Localizer. */ constructor() { super(); /** * Dependencies for the FileUpload component. * @type {object} */ __publicField(this, "dependencies", { "wje-button": Button }); __publicField(this, "className", "FileUpload"); /** * Method to handle file drop event. * @param {Event} event The file drop event object. */ __publicField(this, "handleDrop", (event) => { const fileList = event.dataTransfer.files; this.resetFormState(); this.addFilesToQueue(fileList); }); /** * Method to handle file input change event. * @param {Event} event The file input change event object. */ __publicField(this, "handleInputChange", (event) => { this.resetFormState(); try { this.handleSubmit(event); } catch (err) { } }); /** * Method to create an upload promise. * @param file * @returns {Promise<unknown>} */ __publicField(this, "createUploadPromise", (file) => { return new Promise((resolve, reject) => { this.assertFilesValid(file); let preview; let reader = new FileReader(); reader.onload = (e) => { var _a; this.dispatchEvent( new CustomEvent("file-upload:upload-started", { detail: file, bubbles: true, composed: true }) ); (_a = this.onUploadStarted) == null ? void 0 : _a.call(this, file); preview = this.createPreview(file, reader); this.appendChild(preview); this.uploadFunction(file, preview).then((res) => { var _a2; this.dispatchEvent( new CustomEvent("file-upload:upladed-file-complete", { detail: res, bubbles: true, composed: true }) ); (_a2 = this.onUploadedFileComplete) == null ? void 0 : _a2.call(this, res); this.uploadedFiles.push(res.data); resolve(res); }); }; reader.readAsDataURL(file); }); }); this.localizer = new Localizer(this); this._uploadedFiles = []; this._queuedFiles = []; } /** * Setter for acceptedTypes attribute. * @param {string} value The accepted file types for upload. */ set acceptedTypes(value) { this.setAttribute("accepted-types", value); } /** * Getter for acceptedTypes attribute. * @returns {string} The accepted file types for upload. */ get acceptedTypes() { const accepted = this.getAttribute("accepted-types"); return this.hasAttribute("accepted-types") ? accepted : ""; } /** * Setter for chunkSize attribute. * @param {number} value The chunk size for file upload. */ set chunkSize(value) { this.setAttribute("chunk-size", value); } /** * Getter for chunkSize attribute. * @returns {number} The chunk size for file upload. */ get chunkSize() { const chunk = this.getAttribute("chunk-size"); return this.hasAttribute("chunk-size") ? chunk : 1024 * 1024; } /** * Setter for maxFileSize attribute. * @param {number} value The maximum file size for upload. */ set maxFileSize(value) { this.setAttribute("max-file-size", value); } /** * Getter for maxFileSize attribute. * @returns {number} The maximum file size for upload. */ get maxFileSize() { const fileSize = this.getAttribute("max-file-size"); return this.hasAttribute("max-file-size") ? fileSize * 1024 * 1024 : 1024 * 1024; } /** * Setter for label attribute. * @param {string} value The URL to set as the upload URL. */ set uploadUrl(value) { this.setAttribute("upload-url", value); } /** * Gets the upload URL for the file upload element. * @returns {string} The upload URL for the file upload element. */ get uploadUrl() { return this.getAttribute("upload-url") ?? "/upload"; } /** * Sets the autoProcessFiles attribute. * @param value */ set autoProcessFiles(value) { this.setAttribute("auto-process-files", value); } /** * Gets the autoProcessFiles attribute. * @returns {any|boolean} */ get autoProcessFiles() { return JSON.parse(this.getAttribute("auto-process-files")) ?? true; } /** * Sets the noUploadButton attribute. * @param value */ set noUploadButton(value) { this.setAttribute("no-upload-button", value); } /** * Gets the noUploadButton attribute. * @returns {boolean} */ get noUploadButton() { return this.hasAttribute("no-upload-button"); } /** * Sets the uploaded files. * @param value */ set uploadedFiles(value) { this._uploadedFiles = value; } /** * Return the uploaded files. * @returns {[]} */ get uploadedFiles() { return this._uploadedFiles; } /** * Sets the to-chunk attribute. * @param value */ set toChunk(value) { this.setAttribute("to-chunk", value); } /** * Gets the to-chunk attribute. * @returns {boolean} */ get toChunk() { return this.hasAttribute("to-chunk"); } /** * Getter for cssStyleSheet. * @returns {string} The CSS styles for the component. */ static get cssStyleSheet() { return styles; } /** * Getter for observedAttributes. * @returns {Array} An empty array as no attributes are observed. */ static get observedAttributes() { return []; } /** * Method to setup attributes for the component. */ setupAttributes() { this.isShadowRoot = "open"; } beforeDraw() { console.log("beforeDraw", this.toChunk, !this.toChunk); this.uploadFunction = upload(this.uploadUrl, this.chunkSize, !this.toChunk); } /** * Method to draw the component on the screen. * @returns {DocumentFragment} The fragment containing the component. */ draw() { let fragment = document.createDocumentFragment(); let native = document.createElement("div"); native.classList.add("native-file-upload"); native.setAttribute("part", "native"); let label = document.createElement("div"); label.setAttribute("part", "label"); label.classList.add("file-label"); label.setAttribute("part", "file-label"); let fileList = document.createElement("slot"); fileList.setAttribute("name", "item"); fileList.setAttribute("part", "items"); fileList.classList.add("file-list"); let slot = document.createElement("slot"); label.appendChild(slot); let fileInput = document.createElement("input"); fileInput.setAttribute("type", "file"); fileInput.setAttribute("multiple", ""); fileInput.setAttribute("style", "display:none;"); if (!this.noUploadButton) { let button = document.createElement("wje-button"); button.innerText = this.label || this.localizer.translate("wj.file.upload.button"); button.setAttribute("part", "upload-button"); label.appendChild(button); this.button = button; } native.appendChild(fileInput); native.appendChild(label); native.appendChild(fileList); fragment.appendChild(native); this.native = native; this.fileList = fileList; this.fileInput = fileInput; return fragment; } /** * Method to perform actions after the component is drawn. */ afterDraw() { var _a; (_a = this.button) == null ? void 0 : _a.addEventListener("click", () => { this.fileInput.click(); }); this.fileInput.addEventListener("change", this.handleInputChange); this.native.addEventListener("drop", this.handleDrop); let dragEventCounter = 0; this.native.addEventListener("dragenter", (event) => { event.preventDefault(); if (dragEventCounter === 0) { this.native.classList.add("highlight"); } dragEventCounter += 1; }); this.native.addEventListener("dragover", (event) => { event.preventDefault(); if (dragEventCounter === 0) { dragEventCounter = 1; } }); this.native.addEventListener("dragleave", (event) => { event.preventDefault(); dragEventCounter -= 1; if (dragEventCounter <= 0) { dragEventCounter = 0; this.native.classList.remove("highlight"); } }); this.native.addEventListener("drop", (event) => { event.preventDefault(); dragEventCounter = 0; this.native.classList.remove("highlight"); }); } /** * Method to handle form submission. * @param {Event} event The form submission event. */ handleSubmit(event) { event.preventDefault(); this.addFilesToQueue(this.fileInput.files); } /** * Method to add files to the queue. * @param files */ addFilesToQueue(files) { var _a; this._queuedFiles = [...files]; this.dispatchEvent( new CustomEvent("file-upload:files-added", { detail: files, bubbles: true, composed: true }) ); (_a = this.onAddedFiles) == null ? void 0 : _a.call(this); if (this.autoProcessFiles) { this.uploadFiles(); } this.fileInput.value = ""; } /** * Method to upload files. */ uploadFiles() { if (this._queuedFiles.length === 0) { return; } const uploadPromises = this._queuedFiles.map((file) => this.createUploadPromise(file)); uploadPromises.reduce((prev, curr) => { return prev.then(() => { return curr; }); }, Promise.resolve()).then(() => { var _a; this.dispatchEvent( new CustomEvent("file-upload:all-files-uploaded", { detail: this.uploadedFiles, bubbles: true, composed: true }) ); (_a = this.onAllFilesUploaded) == null ? void 0 : _a.call(this); this._queuedFiles = []; }); } /** * Method to create a preview for the file. * @param {File} file The file for which the preview is to be created. * @param {FileReader} reader The FileReader instance to read the file. * @returns {HTMLElement} The created preview. */ createPreview(file, reader) { let preview = document.createElement("wje-file-upload-item"); preview.setAttribute("slot", "item"); preview.setAttribute("name", file.name); preview.setAttribute("size", file.size); preview.setAttribute("uploaded", "0"); preview.setAttribute("progress", "0"); preview.innerHTML = `<wje-icon slot="img" name="${getFileTypeIcon(file.type.split("/")[1])}" size="large"></wje-icon>`; return preview; } /** * Method to create a thumbnail for the file. * @param {File} file The file for which the thumbnail is to be created. * @param {FileReader} reader The FileReader instance to read the file. * @returns {HTMLElement} The created thumbnail. */ createThumbnail(file, reader) { let img = document.createElement("img"); img.setAttribute("src", reader.result); return img; } /** * Method to validate the files. * @param {File} file The file to be validated. * TODO: alowed types a size limit by malo byt cez attributy */ assertFilesValid(file) { const { name: fileName, size: fileSize } = file; if (!isValidFileType(file, this.acceptedTypes)) { throw new Error(`❌ FILE: "${fileName}" Valid file types are: "${this.acceptedTypes}"`); } if (fileSize > this.maxFileSize) { throw new Error( `❌ File "${fileName}" could not be uploaded. Only images up to ${this.maxFileSize} MB are allowed. Nie je to ${fileSize}` ); } } /** * Method to reset the form state. */ resetFormState() { this.fileList.textContent = ""; } } FileUpload.define("wje-file-upload", FileUpload); export { FileUpload as default }; //# sourceMappingURL=wje-file-upload.js.map