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.

639 lines (638 loc) 21.4 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 WJElement from "./wje-element.js"; import Button from "./wje-button.js"; import { isValidFileType } from "./utils.js"; import { event } from "./event.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", "msword", "vnd.openxmlformats-officedocument.wordprocessingml.document", "vnd.ms-word.document.macroenabled.12", "vnd.openxmlformats-officedocument.wordprocessingml.template", "vnd.ms-word.template.macroenabled.12", "vnd.oasis.opendocument.text" ], name: "file-type-doc" }, { type: [ "xls", "xlsx", "vnd.ms-excel", "vnd.openxmlformats-officedocument.spreadsheetml.sheet", "vnd.ms-excel.sheet.macroenabled.12", "vnd.openxmlformats-officedocument.spreadsheetml.template", "vnd.ms-excel.template.macroenabled.12", "vnd.ms-excel.addin.macroenabled.12", "vnd.ms-excel.sheet.binary.macroenabled.12", "vnd.oasis.opendocument.spreadsheet" ], name: "file-type-xls" }, { type: ["pdf"], name: "file-type-pdf" }, { type: [ "ppt", "pptx", "odp", "vnd.ms-powerpoint", "vnd.openxmlformats-officedocument.presentationml.presentation", "vnd.ms-powerpoint.presentation.macroenabled.12", "vnd.openxmlformats-officedocument.presentationml.slideshow", "vnd.ms-powerpoint.slideshow.macroenabled.12", "vnd.openxmlformats-officedocument.presentationml.template", "vnd.ms-powerpoint.template.macroenabled.12", "vnd.oasis.opendocument.presentation" ], name: "file-type-ppt" } ]; } function getFileTypeIcon(type) { let searchType; if (!type) { return searchType; } if (type.toLowerCase() !== "folder") { fileType().forEach((i) => { if (i.type.includes(type.toLowerCase())) { searchType = i.name; } }); } else { searchType = "folder"; } return searchType; } 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; (offset + uploadedBytes) / file.size * 100; 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}`); } partResponses.push(response2); } catch (error) { console.error("Error uploading chunk:", error); break; } offset += chunkSize; } 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} e The file drop event object. */ __publicField(this, "handleDrop", (e) => { const fileList = e.dataTransfer.files; this.resetFormState(); this.addFilesToQueue(fileList); }); /** * Method to handle file input change event. * @param {Event} e The file input change event object. */ __publicField(this, "handleInputChange", (e) => { const files = Array.from(e.target.files); event.dispatchCustomEvent(this, "wje-file-upload:files-selected", files); this.resetFormState(); try { this.handleSubmit(e); } catch (err) { console.error(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 = () => { var _a; event.dispatchCustomEvent(this, "wje-file-upload:started", file); (_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; res.item = preview; event.dispatchCustomEvent(this, "wje-file-upload:file-uploaded", res); (_a2 = this.onUploadedFileComplete) == null ? void 0 : _a2.call(this, res); this.uploadedFiles.push(res); 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 : "image/*"; } /** * 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"); } /** * Sets the maximum number of files that can be uploaded or managed. * Assigns the specified value to the 'max-files' attribute. * @param {number} value The maximum allowable number of files. */ set maxFiles(value) { this.setAttribute("max-files", value); } /** * Sets the label attribute for the upload button. * @param {string} value */ set label(value) { this.setAttribute("label", value); } /** * Gets the label attribute for the upload button. * @returns {string} */ get label() { return this.getAttribute("label") || ""; } /** * Retrieves the maximum number of files allowed from the `max-files` attribute. * If the attribute is not set or is invalid, defaults to 0. * @returns {number} The maximum number of files allowed. */ get maxFiles() { return parseInt(this.getAttribute("max-files")) || 10; } /** * 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 ["label"]; } attributeChangedCallback(name, oldValue, newValue) { var _a; (_a = super.attributeChangedCallback) == null ? void 0 : _a.call(this, name, oldValue, newValue); if (name === "label" && this.button) { const nextLabel = this.label || this.localizer.translate("wj.file.upload.button"); this.button.innerText = nextLabel; this.button.setAttribute("aria-label", nextLabel); } } /** * Method to setup attributes for the component. */ setupAttributes() { this.isShadowRoot = "open"; this.setAriaState({ role: "group" }); } beforeDraw() { 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", "file-label"); label.classList.add("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;"); fileInput.setAttribute("aria-hidden", "true"); 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"); button.setAttribute("aria-label", button.innerText); 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", (e) => { e.preventDefault(); if (dragEventCounter === 0) { this.native.classList.add("highlight"); } dragEventCounter += 1; }); this.native.addEventListener("dragover", (e) => { e.preventDefault(); if (dragEventCounter === 0) { dragEventCounter = 1; } }); this.native.addEventListener("dragleave", (e) => { e.preventDefault(); dragEventCounter -= 1; if (dragEventCounter <= 0) { dragEventCounter = 0; this.native.classList.remove("highlight"); } }); this.native.addEventListener("drop", (e) => { event.preventDefault(); dragEventCounter = 0; this.native.classList.remove("highlight"); }); this.addEventListener("wje-file-upload-item:remove", (e) => { const file = e.detail; if (!(file instanceof File)) { return; } let count = this.uploadedFiles.length; this.uploadedFiles = this.uploadedFiles.filter((entry) => { var _a2; return ((_a2 = entry == null ? void 0 : entry.file) == null ? void 0 : _a2.lastModified) !== file.lastModified; }); if (count !== this.uploadedFiles.length) event.dispatchCustomEvent(this, "wje-file-upload:file-removed", file); }); } /** * Method to handle form submission. * @param {Event} e The form submission event. */ handleSubmit(e) { e.preventDefault(); this.addFilesToQueue(this.fileInput.files); } /** * Method to add files to the queue. * @param files */ addFilesToQueue(files) { var _a; const currentCount = (Array.isArray(this.uploadedFiles) ? this.uploadedFiles.length : 0) + (Array.isArray(this._queuedFiles) ? this._queuedFiles.length : 0); const newTotal = currentCount + files.length; if (this.maxFiles && newTotal > this.maxFiles) { const detail = { code: "MAX-FILES-EXCEEDED", files, maxFiles: this.maxFiles, currentCount, attemptedToAdd: files.length, allowedRemaining: Math.max(this.maxFiles - currentCount, 0) }; event.dispatchCustomEvent(this, "wje-file-upload:error", detail); return; } this._queuedFiles = [...this._queuedFiles || [], ...files]; event.dispatchCustomEvent(this, "wje-file-upload:files-added", files); (_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; event.dispatchCustomEvent(this, "wje-file-upload:files-uploaded", this.uploadedFiles); (_a = this.onAllFilesUploaded) == null ? void 0 : _a.call(this); this._queuedFiles = []; }).catch((err) => { this._queuedFiles = this._queuedFiles.filter((file) => file !== err.file); event.dispatchCustomEvent(this, "wje-file-upload:error", err); }); } /** * 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"); let fileType2 = file.type ? file.type.split("/")[1] : file.name.split(".").pop(); let icon = getFileTypeIcon(fileType2) || "file"; preview.setAttribute("slot", "item"); preview.setAttribute("name", file.name); preview.setAttribute("size", file.size); preview.setAttribute("uploaded", "0"); preview.innerHTML = `<wje-icon slot="img" name="${icon}" size="large"></wje-icon>`; preview.data = file; 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)) { const err = new Error(""); err.code = "INVALID-FILE-TYPE"; err.file = file; err.acceptedTypes = this.acceptedTypes; throw err; } if (fileSize > this.maxFileSize) { const err = new Error(""); err.code = "FILE-TOO-LARGE"; err.file = file; err.maxFileSize = this.maxFileSize; throw err; } } /** * 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