@trimble-oss/moduswebcomponents
Version:
Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust
652 lines (651 loc) • 25.3 kB
JavaScript
import { h, Host, } from "@stencil/core";
import { convertPropsToClasses } from "./modus-wc-file-dropzone.tailwind";
import { handleShadowDOMStyles } from "../base-component";
import { inheritAriaAttributes } from "../utils";
/**
* File dropzone component that allows users to drag and drop files for upload.
*
* The component supports a `<slot>` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area.
*/
export class ModusWcFileDropzone {
constructor() {
this.inheritedAttributes = {};
/** Tracks if files are being dragged over the dropzone */
this.isDraggingOver = false;
/** Tracks if file has any validation error and the type of error */
this.invalidFile = 'none';
/** Stores the error message when validation fails */
this.errorMessage = '';
/** Tracks if files were successfully uploaded */
this.uploadSuccess = false;
/** Custom CSS class to apply to the file dropzone element */
this.customClass = '';
/** Include state icon for default, validation, upload, and feedback states */
this.includeStateIcon = true;
this.handleDropzoneDrop = (event) => {
var _a;
event.preventDefault();
event.stopPropagation();
this.isDraggingOver = false;
if (this.disabled)
return;
const files = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files;
if (files && files.length > 0) {
// Reset validation state
this.invalidFile = 'none';
this.errorMessage = '';
this.uploadSuccess = false;
// Check file count
if (!this.isValidFileCount(files.length)) {
this.setInputValue('count');
this.errorMessage = this.getErrorMessage('count');
return;
}
// Check total file size
if (!this.isValidFileSize(files)) {
this.setInputValue('size');
this.errorMessage = this.getErrorMessage('size');
return;
}
// Check each file
for (let i = 0; i < files.length; i++) {
// Check file type
if (!this.isValidFileType(files[i])) {
this.setInputValue('type');
this.errorMessage = this.getErrorMessage('type');
return;
}
// Check file name length
if (!this.isValidFileName(files[i])) {
this.setInputValue('name');
this.errorMessage = this.getErrorMessage('name');
return;
}
}
this.uploadSuccess = true;
this.fileSelect.emit(files);
}
};
this.handleDropzoneDragOver = (event) => {
event.preventDefault();
event.stopPropagation();
if (this.disabled)
return;
if (!this.isDraggingOver) {
this.isDraggingOver = true;
}
};
this.handleDropzoneDragLeave = (event) => {
event.preventDefault();
event.stopPropagation();
this.isDraggingOver = false;
};
this.handleDropzoneClick = () => {
var _a;
if (this.disabled)
return;
(_a = this.inputRef) === null || _a === void 0 ? void 0 : _a.click();
};
this.handleDropzoneKeyDown = (event) => {
var _a;
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
if (this.disabled)
return;
(_a = this.inputRef) === null || _a === void 0 ? void 0 : _a.click();
}
};
}
componentWillLoad() {
handleShadowDOMStyles(this.el);
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
/**
* Reset the dropzone to its initial state, clearing all error and success states
*/
async reset() {
this.invalidFile = 'none';
this.errorMessage = '';
this.uploadSuccess = false;
this.isDraggingOver = false;
if (this.inputRef) {
this.inputRef.value = '';
}
return Promise.resolve();
}
// Generate error message based on validation type - called only when validation fails
getErrorMessage(errorType) {
switch (errorType) {
case 'type':
return this.invalidFileTypeMessage || 'File format not accepted';
case 'name':
return 'Filename exceeds maximum length';
case 'count':
return `Maximum number of files allowed is ${this.maxFileCount}`;
case 'size':
return `Total file size exceeds ${this.formatFileSize(this.maxTotalFileSizeBytes)}`;
default:
return 'Validation error';
}
}
isValidFileType(file) {
if (!this.acceptFileTypes)
return true;
const acceptedTypes = this.acceptFileTypes
.split(',')
.map((type) => type.trim().toLowerCase());
const fileName = file.name.toLowerCase();
const fileType = file.type.toLowerCase();
return acceptedTypes.some((acceptedType) => {
if (acceptedType.includes('/*')) {
const mainType = acceptedType.split('/')[0];
return fileType.startsWith(mainType + '/');
}
else if (acceptedType.startsWith('.')) {
return fileName.endsWith(acceptedType);
}
else {
return fileType === acceptedType;
}
});
}
isValidFileName(file) {
if (this.maxFileNameLength === undefined || this.maxFileNameLength === null)
return true;
const fileNameWithoutExtension = file.name.replace(/\.[^/.]+$/, '');
return fileNameWithoutExtension.length <= this.maxFileNameLength;
}
isValidFileCount(fileCount) {
if (this.maxFileCount === undefined || this.maxFileCount === null)
return true;
return fileCount <= this.maxFileCount;
}
isValidFileSize(files) {
if (this.maxTotalFileSizeBytes === undefined ||
this.maxTotalFileSizeBytes === null)
return true;
let totalSize = 0;
for (let i = 0; i < files.length; i++) {
totalSize += files[i].size;
}
return totalSize <= this.maxTotalFileSizeBytes;
}
setInputValue(val, inputElement) {
this.invalidFile = val;
const input = inputElement || this.inputRef;
if (input)
input.value = '';
return false;
}
handleFileChange(event) {
const files = event.target.files;
// Early return if no files
if (!files || files.length === 0) {
return;
}
// Reset validation state
this.invalidFile = 'none';
this.errorMessage = '';
this.uploadSuccess = false;
// Check file count
if (!this.isValidFileCount(files.length)) {
this.setInputValue('count', event.target);
this.errorMessage = this.getErrorMessage('count');
return;
}
// Check total file size
if (!this.isValidFileSize(files)) {
this.setInputValue('size', event.target);
this.errorMessage = this.getErrorMessage('size');
return;
}
// Check each file
for (let i = 0; i < files.length; i++) {
// Check file type
if (!this.isValidFileType(files[i])) {
this.setInputValue('type', event.target);
this.errorMessage = this.getErrorMessage('type');
return;
}
// Check file name length
if (!this.isValidFileName(files[i])) {
this.setInputValue('name', event.target);
this.errorMessage = this.getErrorMessage('name');
return;
}
}
this.fileSelect.emit(files);
this.uploadSuccess = true;
}
getClasses() {
const classList = ['modus-wc-file-input'];
const propClasses = convertPropsToClasses({
disabled: this.disabled,
});
// The order CSS classes are added matters to CSS specificity
if (propClasses)
classList.push(propClasses);
return classList.join(' ');
}
// Helper method to format file size in human-readable format
formatFileSize(bytes) {
if (!bytes)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
getDisplayState() {
var _a, _b, _c;
const defaultDragMessage = this.fileDraggedOverInstructions || 'Drop files here';
const defaultSuccessMessage = this.successMessage || 'Successfully uploaded';
const hasFeedbackInfo = ((_a = this.feedback) === null || _a === void 0 ? void 0 : _a.type) === 'info';
const hasFeedbackError = ((_b = this.feedback) === null || _b === void 0 ? void 0 : _b.type) === 'error';
const hasFeedbackSuccess = ((_c = this.feedback) === null || _c === void 0 ? void 0 : _c.type) === 'success';
const isDragging = this.isDraggingOver || hasFeedbackInfo;
if (isDragging) {
return {
isDragging: true,
isError: false,
isSuccess: false,
message: hasFeedbackInfo ? this.feedback.message : defaultDragMessage,
icon: hasFeedbackInfo ? this.feedback.icon : undefined,
};
}
if (hasFeedbackError) {
return {
isDragging: false,
isError: true,
isSuccess: false,
message: this.feedback.message,
icon: this.feedback.icon,
};
}
if (hasFeedbackSuccess) {
return {
isDragging: false,
isError: false,
isSuccess: true,
message: this.feedback.message,
icon: this.feedback.icon,
};
}
if (this.invalidFile !== 'none') {
return {
isDragging: false,
isError: true,
isSuccess: false,
message: this.errorMessage,
};
}
if (this.uploadSuccess) {
return {
isDragging: false,
isError: false,
isSuccess: true,
message: defaultSuccessMessage,
};
}
return {
isDragging: false,
isError: false,
isSuccess: false,
message: this.instructions,
};
}
render() {
const { isDragging, isError, isSuccess, message, icon: feedbackIcon, } = this.getDisplayState();
const hasState = isDragging || isError || isSuccess;
const showIcon = this.includeStateIcon !== false;
const iconMap = {
dragging: 'cloud_upload',
invalid: 'alert',
success: 'check_circle',
default: 'cloud_upload',
};
const defaultIconState = isDragging
? iconMap.dragging
: isError
? iconMap.invalid
: isSuccess
? iconMap.success
: iconMap.default;
const iconState = feedbackIcon !== null && feedbackIcon !== void 0 ? feedbackIcon : defaultIconState;
const iconClass = isDragging
? 'upload-icon'
: isError
? 'error-icon'
: isSuccess
? 'success-icon'
: 'upload-icon';
const showDropzoneSlot = hasState || this.disabled ? 'none' : 'block';
return (h(Host, { key: '3764c33420e6a2258b5c80b39e83c56cb8eb5f18' }, h("div", { key: '38bb57c72d8cd548d7e841166fca709cea407ecc', class: "modus-wc-file-dropzone" }, h("input", { key: '7bdef7a80528ae10aefc3917cebd252c5b5ab730', accept: this.acceptFileTypes, class: this.getClasses(), disabled: this.disabled, multiple: this.multiple, onChange: (event) => this.handleFileChange(event), ref: (el) => (this.inputRef = el), type: "file" }), h("div", Object.assign({ key: '7c115558c9d22f79ea1d22e2f12e1256295c804f', "aria-disabled": this.disabled ? 'true' : 'false', class: `dropzone-content
${isDragging ? 'dragging-over' : ''}
${!isDragging && isError ? 'invalid-file-type' : ''}
${!isDragging && isSuccess ? 'upload-success' : ''}
${this.customClass || ''}`, onClick: this.handleDropzoneClick, onDragLeave: this.handleDropzoneDragLeave, onDragOver: this.handleDropzoneDragOver, onDrop: this.handleDropzoneDrop, onKeyDown: this.handleDropzoneKeyDown, role: "button", tabindex: this.disabled ? -1 : 0 }, this.inheritedAttributes), h("div", { key: '48233c63e9cc2aacdff1cc296507771aa7a7da5d', class: "default-content" }, showIcon && (h("modus-wc-icon", { key: '0bc6b7ff22bd3b166a7b517a845ccaa7821df3d0', class: iconClass, name: iconState, size: "lg", variant: "solid" })), h("span", { key: '46ca3cc4fcf44a1cbe444a325002615c32343b8d' }, message), h("div", { key: '87cf35c3c86af864d920ad3f9a5dff894c808680', style: { display: showDropzoneSlot } }, h("slot", { key: '8a9b7ef18e24b3fd5b4465691483620eedc1de6d', name: "dropzone" })))))));
}
static get is() { return "modus-wc-file-dropzone"; }
static get originalStyleUrls() {
return {
"$": ["modus-wc-file-dropzone.scss"]
};
}
static get styleUrls() {
return {
"$": ["modus-wc-file-dropzone.css"]
};
}
static get properties() {
return {
"acceptFileTypes": {
"type": "string",
"attribute": "accept-file-types",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Accepted file types (e.g. '.jpg,.png' or 'image/*')"
},
"getter": false,
"setter": false,
"reflect": false
},
"customClass": {
"type": "string",
"attribute": "custom-class",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Custom CSS class to apply to the file dropzone element"
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "''"
},
"disabled": {
"type": "boolean",
"attribute": "disabled",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Disable the file input"
},
"getter": false,
"setter": false,
"reflect": false
},
"feedback": {
"type": "unknown",
"attribute": "feedback",
"mutable": false,
"complexType": {
"original": "IFileDropzoneFeedback",
"resolved": "IFileDropzoneFeedback | undefined",
"references": {
"IFileDropzoneFeedback": {
"location": "import",
"path": "../types",
"id": "src/components/types.ts::IFileDropzoneFeedback"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "External feedback to display info, success, or error state with optional custom icon and message"
},
"getter": false,
"setter": false
},
"fileDraggedOverInstructions": {
"type": "string",
"attribute": "file-dragged-over-instructions",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Custom instructions shown when files are dragged over the dropzone"
},
"getter": false,
"setter": false,
"reflect": false
},
"includeStateIcon": {
"type": "boolean",
"attribute": "include-state-icon",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Include state icon for default, validation, upload, and feedback states"
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "true"
},
"instructions": {
"type": "string",
"attribute": "instructions",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Custom instructions shown as the default dropzone message"
},
"getter": false,
"setter": false,
"reflect": false
},
"invalidFileTypeMessage": {
"type": "string",
"attribute": "invalid-file-type-message",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Custom error message displayed when an invalid file type is selected"
},
"getter": false,
"setter": false,
"reflect": false
},
"maxFileNameLength": {
"type": "number",
"attribute": "max-file-name-length",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Maximum allowed length of filename, will show error if exceeded"
},
"getter": false,
"setter": false,
"reflect": false
},
"maxFileCount": {
"type": "number",
"attribute": "max-file-count",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Maximum number of files allowed, will show error if exceeded"
},
"getter": false,
"setter": false,
"reflect": false
},
"maxTotalFileSizeBytes": {
"type": "number",
"attribute": "max-total-file-size-bytes",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Maximum total file size in bytes allowed, will show error if exceeded"
},
"getter": false,
"setter": false,
"reflect": false
},
"multiple": {
"type": "boolean",
"attribute": "multiple",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Allow multiple file selection"
},
"getter": false,
"setter": false,
"reflect": false
},
"successMessage": {
"type": "string",
"attribute": "success-message",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Success message displayed when files are uploaded successfully"
},
"getter": false,
"setter": false,
"reflect": false
}
};
}
static get states() {
return {
"isDraggingOver": {},
"invalidFile": {},
"errorMessage": {},
"uploadSuccess": {}
};
}
static get events() {
return [{
"method": "fileSelect",
"name": "fileSelect",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event emitted when files are selected"
},
"complexType": {
"original": "FileList",
"resolved": "FileList",
"references": {
"FileList": {
"location": "global",
"id": "global::FileList"
}
}
}
}];
}
static get methods() {
return {
"reset": {
"complexType": {
"signature": "() => Promise<void>",
"parameters": [],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Reset the dropzone to its initial state, clearing all error and success states",
"tags": []
}
}
};
}
static get elementRef() { return "el"; }
}