diff --git a/src/assets/icons/upload_file.svg b/src/assets/icons/upload_file.svg deleted file mode 100644 index 1fc88ee3c..000000000 --- a/src/assets/icons/upload_file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/ui.form-input-file/composables/attrs.composable.js b/src/ui.form-input-file/composables/attrs.composable.js index eb9b5a58f..f0cf2aa8a 100644 --- a/src/ui.form-input-file/composables/attrs.composable.js +++ b/src/ui.form-input-file/composables/attrs.composable.js @@ -6,7 +6,7 @@ import defaultConfig from "../configs/default.config"; export function useAttrs(props) { const { config, getAttrs } = useUI(defaultConfig, () => props.config); - const { dropzoneWrapper, placeholder } = config.value; + const { dropzoneWrapper, placeholder, selectedItem } = config.value; const cvaDropzoneWrapper = cva({ base: dropzoneWrapper.base, @@ -20,6 +20,12 @@ export function useAttrs(props) { compoundVariants: placeholder.compoundVariants, }); + const cvaSelectedItem = cva({ + base: selectedItem.base, + variants: selectedItem.variants, + compoundVariants: selectedItem.compoundVariants, + }); + const dropzoneWrapperClasses = computed(() => cvaDropzoneWrapper({ error: Boolean(props.error), @@ -35,20 +41,25 @@ export function useAttrs(props) { }), ); + const selectedItemClasses = computed(() => + cvaSelectedItem({ + size: props.size, + }), + ); + const labelAttrs = getAttrs("label", { isComponent: true }); const buttonAttrs = getAttrs("button", { isComponent: true }); const dropzoneWrapperAttrs = getAttrs("dropzoneWrapper", { classes: dropzoneWrapperClasses }); const descriptionAttrs = getAttrs("description", { isComponent: true }); + const contentWrapperAttrs = getAttrs("contentWrapper"); const buttonWrapperAttrs = getAttrs("buttonWrapper"); - const placeholderWrapperAttrs = getAttrs("placeholderWrapper"); - const placeholderAttrs = getAttrs("placeholder", { - classes: placeholderClasses, - isComponent: true, - }); - const placeholderIconAttrs = getAttrs("placeholderIcon", { isComponent: true }); + const placeholderAttrs = getAttrs("placeholder", { classes: placeholderClasses }); const clearIconAttrs = getAttrs("clearIcon", { isComponent: true }); + const chooseFileIconAttrs = getAttrs("chooseFileIcon", { isComponent: true }); const chooseFileIconNameAttrs = getAttrs("chooseFileIconName", { isComponent: true }); const inputAttrs = getAttrs("input"); + const fileListAttrs = getAttrs("fileList"); + const selectedItemAttrs = getAttrs("selectedItem", { classes: selectedItemClasses }); return { config, @@ -57,11 +68,13 @@ export function useAttrs(props) { buttonAttrs, dropzoneWrapperAttrs, descriptionAttrs, - buttonWrapperAttrs, - placeholderWrapperAttrs, - placeholderIconAttrs, + contentWrapperAttrs, clearIconAttrs, chooseFileIconNameAttrs, placeholderAttrs, + fileListAttrs, + buttonWrapperAttrs, + selectedItemAttrs, + chooseFileIconAttrs, }; } diff --git a/src/ui.form-input-file/configs/default.config.js b/src/ui.form-input-file/configs/default.config.js index 10d370233..8bca99645 100644 --- a/src/ui.form-input-file/configs/default.config.js +++ b/src/ui.form-input-file/configs/default.config.js @@ -26,10 +26,20 @@ export default /*tw*/ { ], }, description: "text-gray-700", - buttonWrapper: "relative mt-3 flex w-full gap-52 justify-between rounded bg-brand-50 p-4", - placeholderWrapper: "flex items-center gap-2", + contentWrapper: "relative mt-3 flex w-full gap-6 justify-between items-start rounded bg-brand-50 p-4", + fileList: "pr-4 shrink-0 text-gray-700 flex-grow flex flex-col gap-4", placeholder: { - base: "pr-4 shrink-0 text-ellipsis overflow-hidden text-gray-700 text-nowrap", + base: "pr-4 shrink-0 text-gray-700 flex-grow", + variants: { + size: { + sm: "text-sm", + md: "text-base", + lg: "text-lg", + }, + }, + }, + selectedItem: { + base: "pr-4 shrink-0 text-gray-700 flex-grow", variants: { size: { sm: "text-sm", @@ -39,25 +49,25 @@ export default /*tw*/ { }, }, button: "hover:cursor-pointer", - placeholderIcon: "-rotate-45", - placeholderIconName: "attach_file", chooseFileIcon: "", - chooseFileIconName: "upload_file", + chooseFileIconName: "attach_file", clearIcon: "", clearIconName: "close", - dropzoneWrapperHover: "border-gray-400 bg-gray-50", + dropzoneWrapperHover: "border-gray-400", dropzoneWrapperError: "hover:border-red-400 border-red-300", input: "sr-only pointer-events-none size-0 opacity-0", + buttonWrapper: "flex gap-4 items-center", i18n: { sizeError: "File size is too big.", formatError: "Format is not supported.", noFile: "No file selected", - uploadFile: "Upload file", + uploadFile: "Choose file", }, defaultVariants: { size: "md", labelAlign: "topInside", allowedFileTypes: [], - maxFileSize: 100, + multiple: false, + maxFileSize: 0, }, }; diff --git a/src/ui.form-input-file/index.vue b/src/ui.form-input-file/index.vue index 44b7d6d1b..74d64db5e 100644 --- a/src/ui.form-input-file/index.vue +++ b/src/ui.form-input-file/index.vue @@ -3,58 +3,64 @@
-
-
+
+ + +
+ + +
+ +
+ -
- - -
@@ -87,7 +93,7 @@ const props = defineProps({ */ label: { type: String, - default: "Label", + default: "", }, /** @@ -95,7 +101,7 @@ const props = defineProps({ */ description: { type: String, - default: "Some description here", + default: "", }, /** @@ -108,8 +114,16 @@ const props = defineProps({ }, modelValue: { - type: Array, - default: () => [], + type: [Array, File], + default: null, + }, + + /** + * Allow select multiple files. + */ + multiple: { + type: Boolean, + default: UIService.get(defaultConfig, UInputFile).default.multiple, }, /** @@ -175,13 +189,14 @@ const { buttonAttrs, dropzoneWrapperAttrs, descriptionAttrs, - buttonWrapperAttrs, - placeholderWrapperAttrs, - placeholderIconAttrs, + contentWrapperAttrs, clearIconAttrs, chooseFileIconAttrs, placeholderAttrs, inputAttrs, + fileListAttrs, + buttonWrapperAttrs, + selectedItemAttrs, } = useAttrs(props); const i18nGlobal = tm(UInputFile); @@ -189,7 +204,11 @@ const currentLocale = computed(() => merge(defaultConfig.i18n, i18nGlobal, props const currentFiles = computed({ get: () => props.modelValue, - set: (newValue) => emit("update:modelValue", newValue), + set: (newValue) => { + const fallbackValue = props.multiple ? [] : null; + + emit("update:modelValue", newValue || fallbackValue); + }, }); const currentError = computed({ @@ -205,8 +224,11 @@ const extensionNames = computed(() => { return props.allowedFileTypes.map((type) => type.replace(".", "")); }); -const placeholder = computed(() => { - return currentFiles.value.length ? currentFiles.value[0].name : currentLocale.value.noFile; +const isValue = computed(() => { + return ( + (Array.isArray(currentFiles.value) && currentFiles.value.length) || + (!Array.isArray(currentFiles.value) && currentFiles.value) + ); }); const nestedComponentSize = computed(() => { @@ -238,7 +260,7 @@ function validate(file) { const isValidSize = targetFileSize <= props.maxFileSize; - if (!isValidSize) { + if (!isValidSize && props.maxFileSize) { currentError.value = currentLocale.value.sizeError; } @@ -256,11 +278,15 @@ function onChangeFile(event) { return; } - currentFiles.value = Array.from(event.target.files); + currentFiles.value = props.multiple + ? [...currentFiles.value, Array.from(event.target.files).at(0)] + : Array.from(event.target.files).at(0); + + if (fileInputRef.value) fileInputRef.value.value = ""; } function onClickResetFiles() { - currentFiles.value = []; + currentFiles.value = null; if (fileInputRef.value) fileInputRef.value.value = ""; } @@ -268,13 +294,13 @@ function onClickResetFiles() { function onDragOver(event) { event.preventDefault(); - dropZoneRef.value.classList.add(config.value.dropzoneWrapperHover.split(" ")); + dropZoneRef.value.classList.add(...config.value.dropzoneWrapperHover.split(" ")); } function onDragLeave(event) { event.preventDefault(); - dropZoneRef.value.classList.remove(config.value.dropzoneWrapperHover.split(" ")); + dropZoneRef.value.classList.remove(...config.value.dropzoneWrapperHover.split(" ")); } function onDrop(event) { @@ -297,7 +323,7 @@ function onDrop(event) { return; } - currentFiles.value = [targetFile]; + currentFiles.value = props.multiple ? [...currentFiles.value, targetFile] : targetFile; }); } diff --git a/web-types.json b/web-types.json index 829b5df57..8011309c8 100644 --- a/web-types.json +++ b/web-types.json @@ -1,7 +1,7 @@ { "framework": "vue", "name": "vueless", - "version": "0.0.102", + "version": "0.0.103", "contributions": { "html": { "description-markup": "markdown", @@ -3448,7 +3448,7 @@ "kind": "expression", "type": "string" }, - "default": "\"Label\"" + "default": "\"\"" }, { "name": "description", @@ -3457,7 +3457,7 @@ "kind": "expression", "type": "string" }, - "default": "\"Some description here\"" + "default": "\"\"" }, { "name": "labelAlign", @@ -3472,9 +3472,18 @@ "name": "modelValue", "value": { "kind": "expression", - "type": "array" + "type": "array|File" }, - "default": "[]" + "default": "null" + }, + { + "name": "multiple", + "description": "Allow select multiple files.", + "value": { + "kind": "expression", + "type": "boolean" + }, + "default": "false" }, { "name": "maxFileSize", @@ -3483,7 +3492,7 @@ "kind": "expression", "type": "number" }, - "default": "100" + "default": "0" }, { "name": "allowedFileTypes", @@ -3539,6 +3548,11 @@ "name": "update:error" } ], + "slots": [ + { + "name": "left" + } + ], "source": { "module": "vueless/ui.form-input-file/index.vue", "symbol": "default"