bootstrap-vue-next
Version:
Seamless integration of Vue 3, Bootstrap 5, and TypeScript for modern, type-safe UI development
1 lines • 38.2 kB
Source Map (JSON)
{"version":3,"file":"BFormFile-DUd50zn5.mjs","names":[],"sources":["../src/components/BFormFile/BFormFile.vue","../src/components/BFormFile/BFormFile.vue"],"sourcesContent":["<template>\n <div ref=\"rootRef\" v-bind=\"processedAttrs.rootAttrs\" class=\"b-form-file-root\">\n <!-- Optional label -->\n <label\n v-if=\"hasLabelSlot || props.label\"\n class=\"form-label\"\n :class=\"props.labelClass\"\n :for=\"computedId\"\n >\n <slot name=\"label\">\n {{ props.label }}\n </slot>\n </label>\n\n <!-- Drop zone wrapper -->\n <div\n v-if=\"!props.plain\"\n ref=\"dropZoneRef\"\n v-bind=\"processedAttrs.dropZoneAttrs\"\n class=\"b-form-file-wrapper\"\n :class=\"{\n 'b-form-file-dragging': isOverDropZone && !props.noDrop,\n 'b-form-file-has-files': hasFiles,\n }\"\n >\n <!-- Custom file control (mimics Bootstrap native input) -->\n <div\n class=\"b-form-file-control\"\n :class=\"computedClasses\"\n :aria-disabled=\"props.disabled\"\n @click=\"handleControlClick\"\n >\n <!-- Custom browse button (now on LEFT to match Bootstrap v5) -->\n <button\n v-if=\"!props.noButton\"\n :id=\"computedId\"\n ref=\"browseButtonRef\"\n type=\"button\"\n class=\"b-form-file-button\"\n :disabled=\"props.disabled\"\n :aria-label=\"props.ariaLabel\"\n :aria-labelledby=\"props.ariaLabelledby\"\n @click.stop=\"openFileDialog\"\n >\n {{ effectiveBrowseText }}\n </button>\n\n <!-- File name display -->\n <div class=\"b-form-file-text\">\n <slot name=\"file-name\" :files=\"selectedFiles\" :names=\"fileNames\">\n <span v-if=\"hasFiles\">{{ formattedFileNames }}</span>\n <span v-else-if=\"hasPlaceholderSlot || props.placeholder\" class=\"text-muted\">\n <slot name=\"placeholder\">{{ props.placeholder }}</slot>\n </span>\n </slot>\n </div>\n </div>\n\n <!-- Drag overlay (only shown when dragging) -->\n <div v-if=\"isOverDropZone && !props.noDrop\" class=\"b-form-file-drag-overlay\">\n <slot name=\"drop-placeholder\">\n <div class=\"b-form-file-drag-text\">\n {{ effectiveDropPlaceholder }}\n </div>\n </slot>\n </div>\n\n <!-- Hidden input for form submission (positioned behind UI with z-index) -->\n <input\n ref=\"customInputRef\"\n v-bind=\"processedAttrs.inputAttrs\"\n type=\"file\"\n :name=\"props.name\"\n :form=\"props.form\"\n :multiple=\"props.multiple || props.directory\"\n :disabled=\"props.disabled\"\n :required=\"props.required\"\n :accept=\"computedAccept || undefined\"\n :capture=\"props.capture\"\n :directory=\"props.directory || undefined\"\n :webkitdirectory=\"props.directory || undefined\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n style=\"\n position: absolute;\n z-index: -5;\n width: 0;\n height: 0;\n opacity: 0;\n overflow: hidden;\n pointer-events: none;\n \"\n />\n </div>\n\n <!-- Plain mode - simple native input -->\n <input\n v-else\n :id=\"computedId\"\n ref=\"plainInputRef\"\n v-bind=\"processedAttrs.inputAttrs\"\n type=\"file\"\n :class=\"computedPlainClasses\"\n :form=\"props.form\"\n :name=\"props.name\"\n :multiple=\"props.multiple || props.directory\"\n :disabled=\"props.disabled\"\n :capture=\"props.capture\"\n :accept=\"computedAccept || undefined\"\n :required=\"props.required || undefined\"\n :aria-label=\"props.ariaLabel\"\n :aria-labelledby=\"props.ariaLabelledby\"\n :aria-required=\"props.required || undefined\"\n :directory=\"props.directory || undefined\"\n :webkitdirectory=\"props.directory || undefined\"\n @change=\"onPlainChange\"\n />\n\n <!-- External file display (when showFileNames is true and not plain) -->\n <div v-if=\"showExternalDisplay\" class=\"b-form-file-display mt-2\">\n <slot name=\"file-name\" :files=\"selectedFiles\" :names=\"fileNames\">\n <div v-if=\"hasFiles\" class=\"small text-muted\">\n {{ formattedFileNames }}\n </div>\n <div v-else-if=\"hasPlaceholderSlot || props.placeholder\" class=\"small text-muted\">\n <slot name=\"placeholder\">\n {{ props.placeholder }}\n </slot>\n </div>\n </slot>\n </div>\n\n <!-- ARIA live region for screen reader announcements -->\n <div v-if=\"!props.plain\" class=\"visually-hidden\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ ariaLiveMessage }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport {useDropZone, useFileDialog} from '@vueuse/core'\nimport {computed, nextTick, onMounted, ref, type Ref, useAttrs, useTemplateRef, watch} from 'vue'\nimport {useDefaults} from '../../composables/useDefaults'\nimport {useId} from '../../composables/useId'\nimport {useStateClass} from '../../composables/useStateClass'\nimport {isEmptySlot} from '../../utils/dom'\nimport type {BFormFileSlots, BFormFileProps} from '../../types'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst _props = withDefaults(defineProps<Omit<BFormFileProps, 'modelValue'>>(), {\n ariaLabel: undefined,\n ariaLabelledby: undefined,\n accept: '',\n autofocus: false,\n browseText: undefined,\n capture: undefined,\n directory: false,\n disabled: false,\n dropPlaceholder: undefined,\n fileNameFormatter: undefined,\n form: undefined,\n id: undefined,\n label: '',\n labelClass: undefined,\n multiple: false,\n name: undefined,\n noButton: false,\n noDrop: false,\n plain: false,\n placeholder: 'No file chosen',\n required: false,\n showFileNames: false,\n size: undefined,\n state: null,\n})\nconst props = useDefaults(_props, 'BFormFile')\nconst slots = defineSlots<BFormFileSlots>()\n\nconst emit = defineEmits<{\n change: [value: Event]\n}>()\n\nconst modelValue = defineModel<Exclude<BFormFileProps['modelValue'], undefined>>({\n default: null,\n})\n\nconst attrs = useAttrs()\n\nconst processedAttrs = computed(() => {\n // In plain mode, pass all attributes to the input element\n if (props.plain) {\n return {\n rootAttrs: {},\n dropZoneAttrs: {},\n inputAttrs: attrs,\n }\n }\n // In custom mode, split attributes:\n // - class/style go to root (for layout/positioning)\n // - title goes to drop zone (for tooltip on interactive element)\n // - everything else goes to hidden input (for form functionality)\n const {class: rootClass, style: rootStyle, title: dropZoneTitle, ...inputAttrs} = attrs\n const rootAttrs: Record<string, unknown> = {}\n const dropZoneAttrs: Record<string, unknown> = {}\n if (rootClass !== undefined) rootAttrs.class = rootClass\n if (rootStyle !== undefined) rootAttrs.style = rootStyle\n if (dropZoneTitle !== undefined) dropZoneAttrs.title = dropZoneTitle\n return {\n rootAttrs,\n dropZoneAttrs,\n inputAttrs,\n }\n})\n\nconst computedId = useId(() => props.id)\nconst stateClass = useStateClass(() => props.state)\n\n// Refs\nconst rootRef = useTemplateRef('rootRef')\nconst dropZoneRef = useTemplateRef('dropZoneRef')\nconst browseButtonRef = useTemplateRef('browseButtonRef')\nconst plainInputRef = useTemplateRef<HTMLInputElement>('plainInputRef')\nconst customInputRef = useTemplateRef<HTMLInputElement>('customInputRef')\n\n// Computed accept for file type validation\nconst computedAccept = computed(() =>\n typeof props.accept === 'string' ? props.accept : props.accept.join(',')\n)\n\n// VueUse file dialog (uses our hidden input element)\nconst {\n open,\n reset: resetDialog,\n onChange: onDialogChange,\n} = useFileDialog({\n accept: computedAccept.value,\n multiple: props.multiple || props.directory,\n directory: props.directory,\n input: customInputRef as unknown as Ref<HTMLInputElement>,\n})\n\n// VueUse drop zone (replaces manual drag/drop)\n// Note: We don't pass dataTypes because the accept attribute handles validation\n// and there is no reliable way to get MIME types from in all browsers\n// https://github.com/vueuse/vueuse/issues/4523\nconst {isOverDropZone} = useDropZone(dropZoneRef, {\n onDrop: (files) => {\n if (files && !props.noDrop) {\n handleFiles(files)\n }\n },\n multiple: props.multiple || props.directory,\n})\n\n// Computed properties\nconst hasLabelSlot = computed(() => !isEmptySlot(slots.label))\nconst hasPlaceholderSlot = computed(() => !isEmptySlot(slots.placeholder))\n\nconst computedClasses = computed(() => [\n stateClass.value,\n {\n [`form-control-${props.size}`]: props.size !== undefined,\n },\n])\n\nconst computedPlainClasses = computed(() => [\n 'form-control',\n stateClass.value,\n {\n [`form-control-${props.size}`]: props.size !== undefined,\n },\n])\n\n// Selected files (from dialog or managed state)\nconst internalFiles = ref<readonly File[]>([])\n\nconst selectedFiles = computed<readonly File[]>(() => internalFiles.value)\n\nconst hasFiles = computed(() => selectedFiles.value.length > 0)\n\nconst fileNames = computed(() => selectedFiles.value.map((file) => file.name))\n\nconst formattedFileNames = computed(() => {\n if (!hasFiles.value) return ''\n if (props.fileNameFormatter) {\n return props.fileNameFormatter(selectedFiles.value)\n }\n const names = fileNames.value\n if (names.length === 1) return names[0]\n return `${names.length} files selected`\n})\n\nconst showExternalDisplay = computed(\n () => !props.plain && props.showFileNames && (hasFiles.value || props.placeholder)\n)\n\n// ARIA live region message for accessibility\nconst ariaLiveMessage = computed(() => {\n if (!hasFiles.value) return ''\n const count = selectedFiles.value.length\n if (count === 1) {\n return `File selected: ${selectedFiles.value[0]?.name}`\n }\n return `${count} files selected`\n})\n\nconst effectiveBrowseText = computed(() => props.browseText ?? 'Browse')\nconst effectiveDropPlaceholder = computed(() => props.dropPlaceholder ?? 'Drop files here...')\n\n// Validate file against accept criteria\nconst isFileAccepted = (file: File): boolean => {\n if (!computedAccept.value) return true\n\n const acceptTypes = computedAccept.value.split(',').map((type) => type.trim())\n\n return acceptTypes.some((acceptType) => {\n // Extension match (e.g., .pdf)\n if (acceptType.startsWith('.')) {\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase())\n }\n // Exact MIME type match (e.g., image/png)\n if (!acceptType.includes('*')) {\n return file.type === acceptType\n }\n // Wildcard MIME type match (e.g., image/* or */*)\n const slashIndex = acceptType.indexOf('/')\n if (slashIndex === -1) {\n // Malformed wildcard pattern (no '/'): do not match anything\n return false\n }\n const category = acceptType.slice(0, slashIndex)\n // */* should match any MIME type\n if (category === '*') {\n return true\n }\n return file.type.startsWith(`${category}/`)\n })\n}\n\n// File handling\nconst handleFiles = (files: File[] | FileList, nativeEvent?: Event) => {\n let fileArray: File[] = []\n\n if (nativeEvent) {\n // Plain mode: read from the event target (browser already filtered via accept)\n const input = nativeEvent.target as HTMLInputElement\n fileArray = input.files ? Array.from(input.files) : []\n } else {\n // Custom mode (drag & drop or file dialog): manually filter and set on hidden input\n fileArray = Array.from(files).filter((file) => isFileAccepted(file))\n if (customInputRef.value && typeof DataTransfer !== 'undefined') {\n try {\n const dataTransfer = new DataTransfer()\n fileArray.forEach((file) => dataTransfer.items.add(file))\n customInputRef.value.files = dataTransfer.files\n } catch {\n // In environments where DataTransfer is not fully supported, skip syncing files on the input\n }\n }\n }\n\n // Update internal state\n internalFiles.value = fileArray\n\n // Update model value\n if (fileArray.length === 0) {\n modelValue.value = null\n } else if (props.directory || props.multiple) {\n modelValue.value = fileArray\n } else {\n const [firstFile] = fileArray\n if (firstFile) {\n modelValue.value = firstFile\n }\n }\n\n // Emit change event in nextTick to ensure DOM updates\n // In plain mode: forward the native event (has target.files)\n // In custom mode: create CustomEvent with files in detail\n nextTick(() => {\n if (nativeEvent) {\n // Plain mode: forward native event\n emit('change', nativeEvent)\n } else {\n // Custom mode: create CustomEvent with files\n const changeEvent = new CustomEvent('change', {\n bubbles: true,\n cancelable: false,\n detail: {\n files: fileArray,\n target: {files: fileArray},\n },\n })\n // Also attach files directly for easier access\n Object.defineProperty(changeEvent, 'files', {\n value: fileArray,\n enumerable: true,\n })\n emit('change', changeEvent)\n }\n })\n}\n\n// Open file dialog\nconst openFileDialog = () => {\n if (!props.disabled) {\n open({\n accept: computedAccept.value,\n multiple: props.multiple || props.directory,\n directory: props.directory,\n })\n }\n}\n\n// Handle click on control wrapper (make entire control clickable like Bootstrap v5)\nconst handleControlClick = () => {\n // Don't trigger if clicking the button itself (button has its own handler with .stop)\n // Don't trigger if disabled\n if (!props.disabled) {\n openFileDialog()\n }\n}\n\n// Plain mode change handler\nconst onPlainChange = (e: Event) => {\n const input = e.target as HTMLInputElement\n if (input.files) {\n handleFiles(input.files, e) // Pass native event\n }\n}\n\n// Watch dialog files from useFileDialog\nonDialogChange((files) => {\n if (files) {\n handleFiles(files)\n }\n})\n\n// Reset method\nconst reset = () => {\n internalFiles.value = []\n modelValue.value = null\n resetDialog() // This resets the hidden input in custom mode\n if (plainInputRef.value) {\n plainInputRef.value.value = ''\n }\n}\n\n// Focus management\nconst focus = () => {\n if (props.plain) {\n plainInputRef.value?.focus()\n } else {\n browseButtonRef.value?.focus()\n }\n}\n\nconst blur = () => {\n if (props.plain) {\n plainInputRef.value?.blur()\n } else {\n browseButtonRef.value?.blur()\n }\n}\n\n// Autofocus support - initial focus on mount\nonMounted(() => {\n if (props.autofocus) {\n nextTick(() => {\n focus()\n })\n }\n})\n\n// Autofocus support - runtime prop changes\nwatch(\n () => props.autofocus,\n (autofocus) => {\n if (autofocus) {\n focus()\n }\n }\n)\n\n// Watch modelValue changes from parent\nwatch(modelValue, (newValue) => {\n if (newValue === null) {\n internalFiles.value = []\n if (plainInputRef.value) {\n plainInputRef.value.value = ''\n }\n } else if (Array.isArray(newValue)) {\n internalFiles.value = newValue as readonly File[]\n } else {\n internalFiles.value = [newValue] as readonly File[]\n }\n})\n\ndefineExpose({\n blur,\n element: computed(() => (props.plain ? plainInputRef.value : browseButtonRef.value)),\n focus,\n reset,\n})\n</script>\n","<template>\n <div ref=\"rootRef\" v-bind=\"processedAttrs.rootAttrs\" class=\"b-form-file-root\">\n <!-- Optional label -->\n <label\n v-if=\"hasLabelSlot || props.label\"\n class=\"form-label\"\n :class=\"props.labelClass\"\n :for=\"computedId\"\n >\n <slot name=\"label\">\n {{ props.label }}\n </slot>\n </label>\n\n <!-- Drop zone wrapper -->\n <div\n v-if=\"!props.plain\"\n ref=\"dropZoneRef\"\n v-bind=\"processedAttrs.dropZoneAttrs\"\n class=\"b-form-file-wrapper\"\n :class=\"{\n 'b-form-file-dragging': isOverDropZone && !props.noDrop,\n 'b-form-file-has-files': hasFiles,\n }\"\n >\n <!-- Custom file control (mimics Bootstrap native input) -->\n <div\n class=\"b-form-file-control\"\n :class=\"computedClasses\"\n :aria-disabled=\"props.disabled\"\n @click=\"handleControlClick\"\n >\n <!-- Custom browse button (now on LEFT to match Bootstrap v5) -->\n <button\n v-if=\"!props.noButton\"\n :id=\"computedId\"\n ref=\"browseButtonRef\"\n type=\"button\"\n class=\"b-form-file-button\"\n :disabled=\"props.disabled\"\n :aria-label=\"props.ariaLabel\"\n :aria-labelledby=\"props.ariaLabelledby\"\n @click.stop=\"openFileDialog\"\n >\n {{ effectiveBrowseText }}\n </button>\n\n <!-- File name display -->\n <div class=\"b-form-file-text\">\n <slot name=\"file-name\" :files=\"selectedFiles\" :names=\"fileNames\">\n <span v-if=\"hasFiles\">{{ formattedFileNames }}</span>\n <span v-else-if=\"hasPlaceholderSlot || props.placeholder\" class=\"text-muted\">\n <slot name=\"placeholder\">{{ props.placeholder }}</slot>\n </span>\n </slot>\n </div>\n </div>\n\n <!-- Drag overlay (only shown when dragging) -->\n <div v-if=\"isOverDropZone && !props.noDrop\" class=\"b-form-file-drag-overlay\">\n <slot name=\"drop-placeholder\">\n <div class=\"b-form-file-drag-text\">\n {{ effectiveDropPlaceholder }}\n </div>\n </slot>\n </div>\n\n <!-- Hidden input for form submission (positioned behind UI with z-index) -->\n <input\n ref=\"customInputRef\"\n v-bind=\"processedAttrs.inputAttrs\"\n type=\"file\"\n :name=\"props.name\"\n :form=\"props.form\"\n :multiple=\"props.multiple || props.directory\"\n :disabled=\"props.disabled\"\n :required=\"props.required\"\n :accept=\"computedAccept || undefined\"\n :capture=\"props.capture\"\n :directory=\"props.directory || undefined\"\n :webkitdirectory=\"props.directory || undefined\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n style=\"\n position: absolute;\n z-index: -5;\n width: 0;\n height: 0;\n opacity: 0;\n overflow: hidden;\n pointer-events: none;\n \"\n />\n </div>\n\n <!-- Plain mode - simple native input -->\n <input\n v-else\n :id=\"computedId\"\n ref=\"plainInputRef\"\n v-bind=\"processedAttrs.inputAttrs\"\n type=\"file\"\n :class=\"computedPlainClasses\"\n :form=\"props.form\"\n :name=\"props.name\"\n :multiple=\"props.multiple || props.directory\"\n :disabled=\"props.disabled\"\n :capture=\"props.capture\"\n :accept=\"computedAccept || undefined\"\n :required=\"props.required || undefined\"\n :aria-label=\"props.ariaLabel\"\n :aria-labelledby=\"props.ariaLabelledby\"\n :aria-required=\"props.required || undefined\"\n :directory=\"props.directory || undefined\"\n :webkitdirectory=\"props.directory || undefined\"\n @change=\"onPlainChange\"\n />\n\n <!-- External file display (when showFileNames is true and not plain) -->\n <div v-if=\"showExternalDisplay\" class=\"b-form-file-display mt-2\">\n <slot name=\"file-name\" :files=\"selectedFiles\" :names=\"fileNames\">\n <div v-if=\"hasFiles\" class=\"small text-muted\">\n {{ formattedFileNames }}\n </div>\n <div v-else-if=\"hasPlaceholderSlot || props.placeholder\" class=\"small text-muted\">\n <slot name=\"placeholder\">\n {{ props.placeholder }}\n </slot>\n </div>\n </slot>\n </div>\n\n <!-- ARIA live region for screen reader announcements -->\n <div v-if=\"!props.plain\" class=\"visually-hidden\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ ariaLiveMessage }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport {useDropZone, useFileDialog} from '@vueuse/core'\nimport {computed, nextTick, onMounted, ref, type Ref, useAttrs, useTemplateRef, watch} from 'vue'\nimport {useDefaults} from '../../composables/useDefaults'\nimport {useId} from '../../composables/useId'\nimport {useStateClass} from '../../composables/useStateClass'\nimport {isEmptySlot} from '../../utils/dom'\nimport type {BFormFileSlots, BFormFileProps} from '../../types'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst _props = withDefaults(defineProps<Omit<BFormFileProps, 'modelValue'>>(), {\n ariaLabel: undefined,\n ariaLabelledby: undefined,\n accept: '',\n autofocus: false,\n browseText: undefined,\n capture: undefined,\n directory: false,\n disabled: false,\n dropPlaceholder: undefined,\n fileNameFormatter: undefined,\n form: undefined,\n id: undefined,\n label: '',\n labelClass: undefined,\n multiple: false,\n name: undefined,\n noButton: false,\n noDrop: false,\n plain: false,\n placeholder: 'No file chosen',\n required: false,\n showFileNames: false,\n size: undefined,\n state: null,\n})\nconst props = useDefaults(_props, 'BFormFile')\nconst slots = defineSlots<BFormFileSlots>()\n\nconst emit = defineEmits<{\n change: [value: Event]\n}>()\n\nconst modelValue = defineModel<Exclude<BFormFileProps['modelValue'], undefined>>({\n default: null,\n})\n\nconst attrs = useAttrs()\n\nconst processedAttrs = computed(() => {\n // In plain mode, pass all attributes to the input element\n if (props.plain) {\n return {\n rootAttrs: {},\n dropZoneAttrs: {},\n inputAttrs: attrs,\n }\n }\n // In custom mode, split attributes:\n // - class/style go to root (for layout/positioning)\n // - title goes to drop zone (for tooltip on interactive element)\n // - everything else goes to hidden input (for form functionality)\n const {class: rootClass, style: rootStyle, title: dropZoneTitle, ...inputAttrs} = attrs\n const rootAttrs: Record<string, unknown> = {}\n const dropZoneAttrs: Record<string, unknown> = {}\n if (rootClass !== undefined) rootAttrs.class = rootClass\n if (rootStyle !== undefined) rootAttrs.style = rootStyle\n if (dropZoneTitle !== undefined) dropZoneAttrs.title = dropZoneTitle\n return {\n rootAttrs,\n dropZoneAttrs,\n inputAttrs,\n }\n})\n\nconst computedId = useId(() => props.id)\nconst stateClass = useStateClass(() => props.state)\n\n// Refs\nconst rootRef = useTemplateRef('rootRef')\nconst dropZoneRef = useTemplateRef('dropZoneRef')\nconst browseButtonRef = useTemplateRef('browseButtonRef')\nconst plainInputRef = useTemplateRef<HTMLInputElement>('plainInputRef')\nconst customInputRef = useTemplateRef<HTMLInputElement>('customInputRef')\n\n// Computed accept for file type validation\nconst computedAccept = computed(() =>\n typeof props.accept === 'string' ? props.accept : props.accept.join(',')\n)\n\n// VueUse file dialog (uses our hidden input element)\nconst {\n open,\n reset: resetDialog,\n onChange: onDialogChange,\n} = useFileDialog({\n accept: computedAccept.value,\n multiple: props.multiple || props.directory,\n directory: props.directory,\n input: customInputRef as unknown as Ref<HTMLInputElement>,\n})\n\n// VueUse drop zone (replaces manual drag/drop)\n// Note: We don't pass dataTypes because the accept attribute handles validation\n// and there is no reliable way to get MIME types from in all browsers\n// https://github.com/vueuse/vueuse/issues/4523\nconst {isOverDropZone} = useDropZone(dropZoneRef, {\n onDrop: (files) => {\n if (files && !props.noDrop) {\n handleFiles(files)\n }\n },\n multiple: props.multiple || props.directory,\n})\n\n// Computed properties\nconst hasLabelSlot = computed(() => !isEmptySlot(slots.label))\nconst hasPlaceholderSlot = computed(() => !isEmptySlot(slots.placeholder))\n\nconst computedClasses = computed(() => [\n stateClass.value,\n {\n [`form-control-${props.size}`]: props.size !== undefined,\n },\n])\n\nconst computedPlainClasses = computed(() => [\n 'form-control',\n stateClass.value,\n {\n [`form-control-${props.size}`]: props.size !== undefined,\n },\n])\n\n// Selected files (from dialog or managed state)\nconst internalFiles = ref<readonly File[]>([])\n\nconst selectedFiles = computed<readonly File[]>(() => internalFiles.value)\n\nconst hasFiles = computed(() => selectedFiles.value.length > 0)\n\nconst fileNames = computed(() => selectedFiles.value.map((file) => file.name))\n\nconst formattedFileNames = computed(() => {\n if (!hasFiles.value) return ''\n if (props.fileNameFormatter) {\n return props.fileNameFormatter(selectedFiles.value)\n }\n const names = fileNames.value\n if (names.length === 1) return names[0]\n return `${names.length} files selected`\n})\n\nconst showExternalDisplay = computed(\n () => !props.plain && props.showFileNames && (hasFiles.value || props.placeholder)\n)\n\n// ARIA live region message for accessibility\nconst ariaLiveMessage = computed(() => {\n if (!hasFiles.value) return ''\n const count = selectedFiles.value.length\n if (count === 1) {\n return `File selected: ${selectedFiles.value[0]?.name}`\n }\n return `${count} files selected`\n})\n\nconst effectiveBrowseText = computed(() => props.browseText ?? 'Browse')\nconst effectiveDropPlaceholder = computed(() => props.dropPlaceholder ?? 'Drop files here...')\n\n// Validate file against accept criteria\nconst isFileAccepted = (file: File): boolean => {\n if (!computedAccept.value) return true\n\n const acceptTypes = computedAccept.value.split(',').map((type) => type.trim())\n\n return acceptTypes.some((acceptType) => {\n // Extension match (e.g., .pdf)\n if (acceptType.startsWith('.')) {\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase())\n }\n // Exact MIME type match (e.g., image/png)\n if (!acceptType.includes('*')) {\n return file.type === acceptType\n }\n // Wildcard MIME type match (e.g., image/* or */*)\n const slashIndex = acceptType.indexOf('/')\n if (slashIndex === -1) {\n // Malformed wildcard pattern (no '/'): do not match anything\n return false\n }\n const category = acceptType.slice(0, slashIndex)\n // */* should match any MIME type\n if (category === '*') {\n return true\n }\n return file.type.startsWith(`${category}/`)\n })\n}\n\n// File handling\nconst handleFiles = (files: File[] | FileList, nativeEvent?: Event) => {\n let fileArray: File[] = []\n\n if (nativeEvent) {\n // Plain mode: read from the event target (browser already filtered via accept)\n const input = nativeEvent.target as HTMLInputElement\n fileArray = input.files ? Array.from(input.files) : []\n } else {\n // Custom mode (drag & drop or file dialog): manually filter and set on hidden input\n fileArray = Array.from(files).filter((file) => isFileAccepted(file))\n if (customInputRef.value && typeof DataTransfer !== 'undefined') {\n try {\n const dataTransfer = new DataTransfer()\n fileArray.forEach((file) => dataTransfer.items.add(file))\n customInputRef.value.files = dataTransfer.files\n } catch {\n // In environments where DataTransfer is not fully supported, skip syncing files on the input\n }\n }\n }\n\n // Update internal state\n internalFiles.value = fileArray\n\n // Update model value\n if (fileArray.length === 0) {\n modelValue.value = null\n } else if (props.directory || props.multiple) {\n modelValue.value = fileArray\n } else {\n const [firstFile] = fileArray\n if (firstFile) {\n modelValue.value = firstFile\n }\n }\n\n // Emit change event in nextTick to ensure DOM updates\n // In plain mode: forward the native event (has target.files)\n // In custom mode: create CustomEvent with files in detail\n nextTick(() => {\n if (nativeEvent) {\n // Plain mode: forward native event\n emit('change', nativeEvent)\n } else {\n // Custom mode: create CustomEvent with files\n const changeEvent = new CustomEvent('change', {\n bubbles: true,\n cancelable: false,\n detail: {\n files: fileArray,\n target: {files: fileArray},\n },\n })\n // Also attach files directly for easier access\n Object.defineProperty(changeEvent, 'files', {\n value: fileArray,\n enumerable: true,\n })\n emit('change', changeEvent)\n }\n })\n}\n\n// Open file dialog\nconst openFileDialog = () => {\n if (!props.disabled) {\n open({\n accept: computedAccept.value,\n multiple: props.multiple || props.directory,\n directory: props.directory,\n })\n }\n}\n\n// Handle click on control wrapper (make entire control clickable like Bootstrap v5)\nconst handleControlClick = () => {\n // Don't trigger if clicking the button itself (button has its own handler with .stop)\n // Don't trigger if disabled\n if (!props.disabled) {\n openFileDialog()\n }\n}\n\n// Plain mode change handler\nconst onPlainChange = (e: Event) => {\n const input = e.target as HTMLInputElement\n if (input.files) {\n handleFiles(input.files, e) // Pass native event\n }\n}\n\n// Watch dialog files from useFileDialog\nonDialogChange((files) => {\n if (files) {\n handleFiles(files)\n }\n})\n\n// Reset method\nconst reset = () => {\n internalFiles.value = []\n modelValue.value = null\n resetDialog() // This resets the hidden input in custom mode\n if (plainInputRef.value) {\n plainInputRef.value.value = ''\n }\n}\n\n// Focus management\nconst focus = () => {\n if (props.plain) {\n plainInputRef.value?.focus()\n } else {\n browseButtonRef.value?.focus()\n }\n}\n\nconst blur = () => {\n if (props.plain) {\n plainInputRef.value?.blur()\n } else {\n browseButtonRef.value?.blur()\n }\n}\n\n// Autofocus support - initial focus on mount\nonMounted(() => {\n if (props.autofocus) {\n nextTick(() => {\n focus()\n })\n }\n})\n\n// Autofocus support - runtime prop changes\nwatch(\n () => props.autofocus,\n (autofocus) => {\n if (autofocus) {\n focus()\n }\n }\n)\n\n// Watch modelValue changes from parent\nwatch(modelValue, (newValue) => {\n if (newValue === null) {\n internalFiles.value = []\n if (plainInputRef.value) {\n plainInputRef.value.value = ''\n }\n } else if (Array.isArray(newValue)) {\n internalFiles.value = newValue as readonly File[]\n } else {\n internalFiles.value = [newValue] as readonly File[]\n }\n})\n\ndefineExpose({\n blur,\n element: computed(() => (props.plain ? plainInputRef.value : browseButtonRef.value)),\n focus,\n reset,\n})\n</script>\n"],"mappings":"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"}