bootstrap-vue-next 0.45.6 → 0.45.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,6 +76,10 @@ var BFormFile_default = /* @__PURE__ */ defineComponent({
76
76
  props: /* @__PURE__ */ mergeModels({
77
77
  ariaLabel: { default: void 0 },
78
78
  ariaLabelledby: { default: void 0 },
79
+ ariaLiveFormatter: {
80
+ type: Function,
81
+ default: void 0
82
+ },
79
83
  accept: { default: "" },
80
84
  autofocus: {
81
85
  type: Boolean,
@@ -202,6 +206,7 @@ var BFormFile_default = /* @__PURE__ */ defineComponent({
202
206
  const showExternalDisplay = computed(() => !props.plain && props.showFileNames && (hasFiles.value || props.placeholder));
203
207
  const ariaLiveMessage = computed(() => {
204
208
  if (!hasFiles.value) return "";
209
+ if (props.ariaLiveFormatter) return props.ariaLiveFormatter(selectedFiles.value);
205
210
  const count = selectedFiles.value.length;
206
211
  if (count === 1) return `File selected: ${selectedFiles.value[0]?.name}`;
207
212
  return `${count} files selected`;
@@ -409,4 +414,4 @@ var BFormFile_default = /* @__PURE__ */ defineComponent({
409
414
  //#endregion
410
415
  export { BFormFile_default as t };
411
416
 
412
- //# sourceMappingURL=BFormFile-EXRrATZm.mjs.map
417
+ //# sourceMappingURL=BFormFile-CZebm0Cp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BFormFile-CZebm0Cp.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 {BFormFileProps, BFormFileSlots} 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 ariaLiveFormatter: 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 if (props.ariaLiveFormatter) {\n return props.ariaLiveFormatter(selectedFiles.value)\n }\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 {BFormFileProps, BFormFileSlots} 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 ariaLiveFormatter: 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 if (props.ariaLiveFormatter) {\n return props.ariaLiveFormatter(selectedFiles.value)\n }\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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"}
@@ -77,6 +77,10 @@ var BFormFile_default = /* @__PURE__ */ (0, vue.defineComponent)({
77
77
  props: /* @__PURE__ */ (0, vue.mergeModels)({
78
78
  ariaLabel: { default: void 0 },
79
79
  ariaLabelledby: { default: void 0 },
80
+ ariaLiveFormatter: {
81
+ type: Function,
82
+ default: void 0
83
+ },
80
84
  accept: { default: "" },
81
85
  autofocus: {
82
86
  type: Boolean,
@@ -203,6 +207,7 @@ var BFormFile_default = /* @__PURE__ */ (0, vue.defineComponent)({
203
207
  const showExternalDisplay = (0, vue.computed)(() => !props.plain && props.showFileNames && (hasFiles.value || props.placeholder));
204
208
  const ariaLiveMessage = (0, vue.computed)(() => {
205
209
  if (!hasFiles.value) return "";
210
+ if (props.ariaLiveFormatter) return props.ariaLiveFormatter(selectedFiles.value);
206
211
  const count = selectedFiles.value.length;
207
212
  if (count === 1) return `File selected: ${selectedFiles.value[0]?.name}`;
208
213
  return `${count} files selected`;
@@ -415,4 +420,4 @@ Object.defineProperty(exports, "BFormFile_default", {
415
420
  }
416
421
  });
417
422
 
418
- //# sourceMappingURL=BFormFile-X-BQsYko.js.map
423
+ //# sourceMappingURL=BFormFile-MISLntyJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BFormFile-MISLntyJ.js","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 {BFormFileProps, BFormFileSlots} 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 ariaLiveFormatter: 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 if (props.ariaLiveFormatter) {\n return props.ariaLiveFormatter(selectedFiles.value)\n }\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 {BFormFileProps, BFormFileSlots} 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 ariaLiveFormatter: 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 if (props.ariaLiveFormatter) {\n return props.ariaLiveFormatter(selectedFiles.value)\n }\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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"}
@@ -43,7 +43,7 @@ import { t as BFormSelectOption_default } from "./BFormSelectOption-DFN4lPRh.mjs
43
43
  import { n as BFormDatalist_default, t as BFormFloatingLabel_default } from "./BForm-DJu2hDV_.mjs";
44
44
  import { i as BFormInvalidFeedback_default, n as BFormText_default, r as BFormRow_default, t as BFormValidFeedback_default } from "./BFormValidFeedback-CBYxQLDz.mjs";
45
45
  import { n as BFormCheckbox_default, t as BFormCheckboxGroup_default } from "./BFormCheckbox-Baf_9BSc.mjs";
46
- import { t as BFormFile_default } from "./BFormFile-EXRrATZm.mjs";
46
+ import { t as BFormFile_default } from "./BFormFile-CZebm0Cp.mjs";
47
47
  import { t as BFormGroup_default } from "./BFormGroup-vysRPb99.mjs";
48
48
  import { n as BFormRadio_default, t as BFormRadioGroup_default } from "./BFormRadio-COc_IbxC.mjs";
49
49
  import { t as BFormRating_default } from "./BFormRating-CIgPVsH_.mjs";
@@ -44,7 +44,7 @@ const require_BFormSelectOption = require("./BFormSelectOption-DrwJucNf.js");
44
44
  const require_BForm$1 = require("./BForm-6JlWdQMO.js");
45
45
  const require_BFormValidFeedback = require("./BFormValidFeedback-Kwxh798d.js");
46
46
  const require_BFormCheckbox = require("./BFormCheckbox-D5ySHMJ8.js");
47
- const require_BFormFile = require("./BFormFile-X-BQsYko.js");
47
+ const require_BFormFile = require("./BFormFile-MISLntyJ.js");
48
48
  const require_BFormGroup = require("./BFormGroup-Bk1t-D1N.js");
49
49
  const require_BFormRadio = require("./BFormRadio-B2yu-o2v.js");
50
50
  const require_BFormRating = require("./BFormRating-CSdRxN5z.js");
@@ -1,4 +1,4 @@
1
- import { BFormFileSlots, BFormFileProps } from '../../types';
1
+ import { BFormFileProps, BFormFileSlots } from '../../types';
2
2
  type __VLS_Props = Omit<BFormFileProps, 'modelValue'>;
3
3
  type __VLS_PublicProps = {
4
4
  modelValue?: Exclude<BFormFileProps['modelValue'], undefined>;
@@ -42,6 +42,7 @@ declare const __VLS_component: import('vue').DefineComponent<__VLS_PublicProps,
42
42
  multiple: boolean;
43
43
  plain: boolean;
44
44
  ariaLabelledby: string;
45
+ ariaLiveFormatter: (files: readonly File[]) => string;
45
46
  accept: string | readonly string[];
46
47
  browseText: string;
47
48
  capture: "user" | "environment";
@@ -1,4 +1,4 @@
1
- import { BFormFileSlots, BFormFileProps } from '../../types';
1
+ import { BFormFileProps, BFormFileSlots } from '../../types';
2
2
  type __VLS_Props = Omit<BFormFileProps, 'modelValue'>;
3
3
  type __VLS_PublicProps = {
4
4
  modelValue?: Exclude<BFormFileProps['modelValue'], undefined>;
@@ -42,6 +42,7 @@ declare const __VLS_component: import('vue').DefineComponent<__VLS_PublicProps,
42
42
  multiple: boolean;
43
43
  plain: boolean;
44
44
  ariaLabelledby: string;
45
+ ariaLiveFormatter: (files: readonly File[]) => string;
45
46
  accept: string | readonly string[];
46
47
  browseText: string;
47
48
  capture: "user" | "environment";
@@ -1,2 +1,2 @@
1
- import { t as BFormFile_default } from "../../../BFormFile-EXRrATZm.mjs";
1
+ import { t as BFormFile_default } from "../../../BFormFile-CZebm0Cp.mjs";
2
2
  export { BFormFile_default as BFormFile };
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_BFormFile = require("../../../BFormFile-X-BQsYko.js");
2
+ const require_BFormFile = require("../../../BFormFile-MISLntyJ.js");
3
3
  exports.BFormFile = require_BFormFile.BFormFile_default;
@@ -39,7 +39,7 @@ import { t as BFormSelectOption_default } from "../../BFormSelectOption-DFN4lPRh
39
39
  import { n as BFormDatalist_default, t as BFormFloatingLabel_default } from "../../BForm-DJu2hDV_.mjs";
40
40
  import { i as BFormInvalidFeedback_default, n as BFormText_default, r as BFormRow_default, t as BFormValidFeedback_default } from "../../BFormValidFeedback-CBYxQLDz.mjs";
41
41
  import { n as BFormCheckbox_default, t as BFormCheckboxGroup_default } from "../../BFormCheckbox-Baf_9BSc.mjs";
42
- import { t as BFormFile_default } from "../../BFormFile-EXRrATZm.mjs";
42
+ import { t as BFormFile_default } from "../../BFormFile-CZebm0Cp.mjs";
43
43
  import { t as BFormGroup_default } from "../../BFormGroup-vysRPb99.mjs";
44
44
  import "./BFormInput/index.mjs";
45
45
  import { n as BFormRadio_default, t as BFormRadioGroup_default } from "../../BFormRadio-COc_IbxC.mjs";
@@ -40,7 +40,7 @@ const require_BFormSelectOption = require("../../BFormSelectOption-DrwJucNf.js")
40
40
  const require_BForm$1 = require("../../BForm-6JlWdQMO.js");
41
41
  const require_BFormValidFeedback = require("../../BFormValidFeedback-Kwxh798d.js");
42
42
  const require_BFormCheckbox = require("../../BFormCheckbox-D5ySHMJ8.js");
43
- const require_BFormFile = require("../../BFormFile-X-BQsYko.js");
43
+ const require_BFormFile = require("../../BFormFile-MISLntyJ.js");
44
44
  const require_BFormGroup = require("../../BFormGroup-Bk1t-D1N.js");
45
45
  require("./BFormInput/index.umd.js");
46
46
  const require_BFormRadio = require("../../BFormRadio-B2yu-o2v.js");
@@ -336,6 +336,10 @@ export interface BFormDatalistProps<Item = Record<string, unknown> | string | nu
336
336
  export interface BFormFileProps {
337
337
  ariaLabel?: string;
338
338
  ariaLabelledby?: string;
339
+ /**
340
+ * Serves the purpose of adding a custom message, as well as i18n support
341
+ */
342
+ ariaLiveFormatter?: (files: readonly File[]) => string;
339
343
  accept?: string | readonly string[];
340
344
  autofocus?: boolean;
341
345
  browseText?: string;
@@ -343,6 +347,9 @@ export interface BFormFileProps {
343
347
  directory?: boolean;
344
348
  disabled?: boolean;
345
349
  dropPlaceholder?: string;
350
+ /**
351
+ * Serves the purpose of adding a custom message, as well as i18n support
352
+ */
346
353
  fileNameFormatter?: (files: readonly File[]) => string;
347
354
  form?: string;
348
355
  id?: string;
@@ -336,6 +336,10 @@ export interface BFormDatalistProps<Item = Record<string, unknown> | string | nu
336
336
  export interface BFormFileProps {
337
337
  ariaLabel?: string;
338
338
  ariaLabelledby?: string;
339
+ /**
340
+ * Serves the purpose of adding a custom message, as well as i18n support
341
+ */
342
+ ariaLiveFormatter?: (files: readonly File[]) => string;
339
343
  accept?: string | readonly string[];
340
344
  autofocus?: boolean;
341
345
  browseText?: string;
@@ -343,6 +347,9 @@ export interface BFormFileProps {
343
347
  directory?: boolean;
344
348
  disabled?: boolean;
345
349
  dropPlaceholder?: string;
350
+ /**
351
+ * Serves the purpose of adding a custom message, as well as i18n support
352
+ */
346
353
  fileNameFormatter?: (files: readonly File[]) => string;
347
354
  form?: string;
348
355
  id?: string;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "bootstrap-vue-next",
3
3
  "displayName": "BootstrapVueNext",
4
4
  "description": "Seamless integration of Vue 3, Bootstrap 5, and TypeScript for modern, type-safe UI development",
5
- "version": "0.45.6",
5
+ "version": "0.45.7",
6
6
  "license": "MIT",
7
7
  "main": "./dist/bootstrap-vue-next.umd.js",
8
8
  "module": "./dist/bootstrap-vue-next.mjs",
@@ -1 +0,0 @@
1
- {"version":3,"file":"BFormFile-EXRrATZm.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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"BFormFile-X-BQsYko.js","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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"}