vectis-ui 0.10.1 → 0.10.2
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.
- package/dist/components/VAccordion/VAccordion.js.map +1 -1
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.d.ts +6 -21
- package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/context.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.d.ts +2 -12
- package/dist/components/VAvatar/VAvatar.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.d.ts +2 -2
- package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/context.d.ts +1 -5
- package/dist/components/VAvatar/context.js.map +1 -1
- package/dist/components/VBadge/VBadge.d.ts +13 -27
- package/dist/components/VBadge/VBadge.js.map +1 -1
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +3 -9
- package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButton.d.ts +10 -17
- package/dist/components/VButton/VButton.js.map +1 -1
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +6 -15
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/context.d.ts +1 -9
- package/dist/components/VButton/context.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.d.ts +9 -24
- package/dist/components/VCalendar/VCalendar.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.d.ts +4 -13
- package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +1 -5
- package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/color.d.ts +1 -7
- package/dist/components/VCalendar/color.js.map +1 -1
- package/dist/components/VCalendar/edgeStep.d.ts +5 -15
- package/dist/components/VCalendar/edgeStep.js.map +1 -1
- package/dist/components/VCalendar/gesture.d.ts +12 -36
- package/dist/components/VCalendar/gesture.js.map +1 -1
- package/dist/components/VCalendar/keyboard.d.ts +5 -42
- package/dist/components/VCalendar/keyboard.js.map +1 -1
- package/dist/components/VCalendar/layout.d.ts +38 -199
- package/dist/components/VCalendar/layout.js.map +1 -1
- package/dist/components/VCalendar/timeGrid.d.ts +3 -25
- package/dist/components/VCalendar/timeGrid.js.map +1 -1
- package/dist/components/VCalendar/types.d.ts +6 -23
- package/dist/components/VCarousel/VCarousel.d.ts +25 -96
- package/dist/components/VCarousel/VCarousel.js.map +1 -1
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.d.ts +1 -6
- package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/context.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.d.ts +1 -5
- package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VChip/VChip.d.ts +2 -8
- package/dist/components/VChip/VChip.js.map +1 -1
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.d.ts +21 -59
- package/dist/components/VCombobox/VCombobox.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +117 -119
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxSeparator.js.map +1 -1
- package/dist/components/VCombobox/VComboboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/infiniteScroll.d.ts +4 -10
- package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.d.ts +11 -36
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +23 -20
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.d.ts +12 -21
- package/dist/components/VDateInput/VDateInput.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/mask.d.ts +14 -59
- package/dist/components/VDateInput/mask.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.d.ts +6 -12
- package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDatePicker/keyboard.d.ts +10 -42
- package/dist/components/VDatePicker/keyboard.js.map +1 -1
- package/dist/components/VDatePicker/names.d.ts +8 -22
- package/dist/components/VDatePicker/names.js.map +1 -1
- package/dist/components/VDialog/VDialog.d.ts +5 -10
- package/dist/components/VDialog/VDialog.js.map +1 -1
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +51 -53
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.d.ts +2 -3
- package/dist/components/VDialog/VDialogAlert.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.d.ts +11 -37
- package/dist/components/VFileInput/VFileInput.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/truncate.d.ts +3 -23
- package/dist/components/VFileInput/truncate.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.d.ts +15 -65
- package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFilePicker/fileKind.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.d.ts +6 -15
- package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VHotkeys/platform.d.ts +10 -49
- package/dist/components/VHotkeys/platform.js.map +1 -1
- package/dist/components/VIcon/VIcon.d.ts +7 -19
- package/dist/components/VIcon/VIcon.js.map +1 -1
- package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/iconProps.d.ts +5 -19
- package/dist/components/VIcon/iconProps.js.map +1 -1
- package/dist/components/VIcon/resolver.d.ts +15 -43
- package/dist/components/VIcon/resolver.js.map +1 -1
- package/dist/components/VIcon/types.d.ts +7 -34
- package/dist/components/VIconButton/VIconButton.d.ts +2 -3
- package/dist/components/VIconButton/VIconButton.js.map +1 -1
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInput.d.ts +14 -29
- package/dist/components/VInput/VInput.js.map +1 -1
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInputGroup.js.map +1 -1
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +34 -50
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/context.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.d.ts +8 -16
- package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +55 -63
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.d.ts +1 -6
- package/dist/components/VMenu/VMenuGroup.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.d.ts +0 -5
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +1 -6
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuSeparator.js.map +1 -1
- package/dist/components/VMenu/VMenuSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/context.d.ts +3 -11
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VPagination/VPagination.d.ts +9 -20
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VPopover/VPopover.d.ts +7 -18
- package/dist/components/VPopover/VPopover.js.map +1 -1
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +23 -27
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.d.ts +2 -6
- package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.d.ts +4 -15
- package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VRadio/VRadio.d.ts +1 -5
- package/dist/components/VRadio/VRadio.js.map +1 -1
- package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.d.ts +2 -15
- package/dist/components/VSeparator/VSeparator.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.d.ts +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +1 -5
- package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +1 -5
- package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +80 -75
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationSeparator.js.map +1 -1
- package/dist/components/VSideNavigation/context.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +6 -19
- package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSlider/VSlider.d.ts +5 -13
- package/dist/components/VSlider/VSlider.js.map +1 -1
- package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/state.d.ts +4 -17
- package/dist/components/VSnackbar/state.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.d.ts +3 -5
- package/dist/components/VSpinner/VSpinner.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTab.d.ts +1 -5
- package/dist/components/VTabs/VTab.js.map +1 -1
- package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabs.d.ts +5 -17
- package/dist/components/VTabs/VTabs.js.map +1 -1
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/context.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.d.ts +13 -20
- package/dist/components/VTextarea/VTextarea.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.d.ts +8 -21
- package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/search.d.ts +3 -34
- package/dist/components/VTimeInput/search.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.d.ts +9 -31
- package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimePicker/limits.d.ts +18 -45
- package/dist/components/VTimePicker/limits.js.map +1 -1
- package/dist/components/VToast/VToast.js.map +1 -1
- package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/VToaster.js.map +1 -1
- package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/state.d.ts +2 -7
- package/dist/components/VToast/state.js.map +1 -1
- package/dist/components/VToggle/VToggle.d.ts +5 -9
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.d.ts +1 -9
- package/dist/components/VToggle/VToggleItem.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/context.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.d.ts +3 -16
- package/dist/components/VTooltip/VTooltip.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTypography/VTypography.d.ts +4 -8
- package/dist/components/VTypography/VTypography.js.map +1 -1
- package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useAriaLabel.js.map +1 -1
- package/dist/composables/useClearable.d.ts +2 -8
- package/dist/composables/useClearable.js.map +1 -1
- package/dist/composables/useControlShape.js.map +1 -1
- package/dist/composables/useDetailsOpen.d.ts +2 -10
- package/dist/composables/useDetailsOpen.js.map +1 -1
- package/dist/composables/useFieldIds.js.map +1 -1
- package/dist/composables/useFieldPanel.d.ts +4 -17
- package/dist/composables/useFieldPanel.js.map +1 -1
- package/dist/composables/useFileDrop.js.map +1 -1
- package/dist/composables/useFileField.d.ts +1 -5
- package/dist/composables/useFileField.js.map +1 -1
- package/dist/composables/useFocusoutDismiss.js.map +1 -1
- package/dist/composables/useIconClickHandlers.d.ts +4 -19
- package/dist/composables/useIconClickHandlers.js.map +1 -1
- package/dist/composables/useInertLink.d.ts +1 -1
- package/dist/composables/useInertLink.js.map +1 -1
- package/dist/composables/useLiveAnnouncer.js.map +1 -1
- package/dist/composables/useMaskedField.d.ts +12 -27
- package/dist/composables/useMaskedField.js.map +1 -1
- package/dist/composables/usePopover.d.ts +3 -11
- package/dist/composables/usePopover.js.map +1 -1
- package/dist/composables/useProgressValue.js.map +1 -1
- package/dist/composables/useRootAttrs.d.ts +0 -9
- package/dist/composables/useRootAttrs.js +11 -4
- package/dist/composables/useRootAttrs.js.map +1 -1
- package/dist/composables/useSlotNodes.d.ts +3 -14
- package/dist/composables/useSlotNodes.js.map +1 -1
- package/dist/composables/useTextLimit.d.ts +4 -16
- package/dist/composables/useTextLimit.js.map +1 -1
- package/dist/composables/useTimer.d.ts +2 -15
- package/dist/composables/useTimer.js.map +1 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/state.d.ts +9 -38
- package/dist/i18n/state.js.map +1 -1
- package/dist/i18n/types.d.ts +19 -81
- package/dist/icons.d.ts +2 -12
- package/dist/tokens/css.d.ts +3 -16
- package/dist/tokens/css.js.map +1 -1
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/primitives.js.map +1 -1
- package/dist/tokens/semantic.d.ts +1 -23
- package/dist/tokens/semantic.js.map +1 -1
- package/dist/tokens/themes/dark.js.map +1 -1
- package/dist/tokens/types.d.ts +2 -12
- package/dist/tokens/types.js.map +1 -1
- package/dist/types.d.ts +3 -21
- package/dist/utils/array.d.ts +2 -4
- package/dist/utils/array.js.map +1 -1
- package/dist/utils/arrowNav.d.ts +2 -26
- package/dist/utils/arrowNav.js.map +1 -1
- package/dist/utils/chip.d.ts +1 -8
- package/dist/utils/chip.js.map +1 -1
- package/dist/utils/clock.d.ts +16 -39
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/css.d.ts +5 -16
- package/dist/utils/css.js.map +1 -1
- package/dist/utils/date.d.ts +17 -60
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/direction.d.ts +2 -9
- package/dist/utils/direction.js.map +1 -1
- package/dist/utils/env.d.ts +2 -15
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/file.d.ts +8 -46
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/focus.d.ts +1 -14
- package/dist/utils/focus.js +1 -6
- package/dist/utils/focus.js.map +1 -1
- package/dist/utils/hostWarns.d.ts +3 -8
- package/dist/utils/hostWarns.js.map +1 -1
- package/dist/utils/ids.d.ts +2 -7
- package/dist/utils/ids.js.map +1 -1
- package/dist/utils/matcher.d.ts +1 -8
- package/dist/utils/matcher.js.map +1 -1
- package/dist/utils/memo.d.ts +1 -11
- package/dist/utils/memo.js.map +1 -1
- package/dist/utils/number.d.ts +1 -7
- package/dist/utils/number.js.map +1 -1
- package/dist/utils/text.d.ts +4 -23
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/time.d.ts +4 -12
- package/dist/utils/time.js.map +1 -1
- package/dist/utils/vnode.d.ts +0 -10
- package/dist/utils/vnode.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VDialogAlert.js","names":[],"sources":["../../../src/components/VDialog/VDialogAlert.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n *
|
|
1
|
+
{"version":3,"file":"VDialogAlert.js","names":[],"sources":["../../../src/components/VDialog/VDialogAlert.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Fix VDialog to alertdialog semantics and require an explicit answer instead of light\n * dismissal.\n */\n\nimport { computed, ref } from 'vue'\n\nimport VDialog from './VDialog.vue'\nimport type { DialogTriggerProps } from './VDialog.vue'\n\ninterface DialogAlertProps {\n /**\n * The question being asked, which also names the dialog for assistive technology.\n * It is ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, spelling out the consequences of the answer. */\n subtitle?: string\n /**\n * How wide the alert is: a number is read as pixels, a string as any CSS length. Left out, it\n * takes the `--vectis-control-size-dialog-width` token, 400px by default.\n */\n width?: number | string\n}\n\nwithDefaults(defineProps<DialogAlertProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n})\n\n/** Whether the alert is showing. It starts closed, and closing writes back to it. */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** What the alert says. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /**\n * Extra controls in the header, where a dialog puts them before its cross: a link to help,\n * for instance. An alert has no cross, so they sit at the end of the header alone.\n */\n 'header-actions'?(): unknown\n /**\n * The buttons that answer the alert. They are not optional: nothing else can close\n * this dialog.\n */\n footer?(): unknown\n /**\n * The button that opens the alert. Bind the `triggerProps` it receives onto it.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\n/*\n * This component is VDialog with its options fixed, so a ref on it has to answer the same way;\n * without this, the one dialog that CANNOT be dismissed by Escape or by a click outside would\n * also be the one a consumer could not close from code.\n */\nconst dialogRef = ref<InstanceType<typeof VDialog> | null>(null)\n\ndefineExpose({\n /**\n * Opens the alert, exactly as setting `open` does. The opening lands on the next tick:\n * the promise returned settles once the alert is showing.\n */\n show: (): Promise<void> => dialogRef.value?.show() ?? Promise.resolve(),\n /** Closes it. The footer's buttons are the reader's only way out; this is yours. */\n close: () => dialogRef.value?.close(),\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: computed(() => dialogRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <VDialog\n ref=\"dialogRef\"\n v-model:open=\"open\"\n role=\"alertdialog\"\n :title=\"title\"\n :subtitle=\"subtitle\"\n :width=\"width\"\n hide-close\n persistent-backdrop\n persistent-escape\n >\n <template v-if=\"$slots.trigger\" #trigger=\"{ triggerProps }\">\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n </template>\n <template v-if=\"$slots.header\" #header>\n <slot name=\"header\" />\n </template>\n <template v-if=\"$slots['header-actions']\" #header-actions>\n <slot name=\"header-actions\" />\n </template>\n <slot />\n <template v-if=\"$slots.footer\" #footer>\n <slot name=\"footer\" />\n </template>\n </VDialog>\n</template>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VDialogAlert.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VDialog/VDialogAlert.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n *
|
|
1
|
+
{"version":3,"file":"VDialogAlert.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VDialog/VDialogAlert.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Fix VDialog to alertdialog semantics and require an explicit answer instead of light\n * dismissal.\n */\n\nimport { computed, ref } from 'vue'\n\nimport VDialog from './VDialog.vue'\nimport type { DialogTriggerProps } from './VDialog.vue'\n\ninterface DialogAlertProps {\n /**\n * The question being asked, which also names the dialog for assistive technology.\n * It is ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, spelling out the consequences of the answer. */\n subtitle?: string\n /**\n * How wide the alert is: a number is read as pixels, a string as any CSS length. Left out, it\n * takes the `--vectis-control-size-dialog-width` token, 400px by default.\n */\n width?: number | string\n}\n\nwithDefaults(defineProps<DialogAlertProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n})\n\n/** Whether the alert is showing. It starts closed, and closing writes back to it. */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** What the alert says. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /**\n * Extra controls in the header, where a dialog puts them before its cross: a link to help,\n * for instance. An alert has no cross, so they sit at the end of the header alone.\n */\n 'header-actions'?(): unknown\n /**\n * The buttons that answer the alert. They are not optional: nothing else can close\n * this dialog.\n */\n footer?(): unknown\n /**\n * The button that opens the alert. Bind the `triggerProps` it receives onto it.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\n/*\n * This component is VDialog with its options fixed, so a ref on it has to answer the same way;\n * without this, the one dialog that CANNOT be dismissed by Escape or by a click outside would\n * also be the one a consumer could not close from code.\n */\nconst dialogRef = ref<InstanceType<typeof VDialog> | null>(null)\n\ndefineExpose({\n /**\n * Opens the alert, exactly as setting `open` does. The opening lands on the next tick:\n * the promise returned settles once the alert is showing.\n */\n show: (): Promise<void> => dialogRef.value?.show() ?? Promise.resolve(),\n /** Closes it. The footer's buttons are the reader's only way out; this is yours. */\n close: () => dialogRef.value?.close(),\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: computed(() => dialogRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <VDialog\n ref=\"dialogRef\"\n v-model:open=\"open\"\n role=\"alertdialog\"\n :title=\"title\"\n :subtitle=\"subtitle\"\n :width=\"width\"\n hide-close\n persistent-backdrop\n persistent-escape\n >\n <template v-if=\"$slots.trigger\" #trigger=\"{ triggerProps }\">\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n </template>\n <template v-if=\"$slots.header\" #header>\n <slot name=\"header\" />\n </template>\n <template v-if=\"$slots['header-actions']\" #header-actions>\n <slot name=\"header-actions\" />\n </template>\n <slot />\n <template v-if=\"$slots.footer\" #footer>\n <slot name=\"footer\" />\n </template>\n </VDialog>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAiCA,IAAM,IAAO,EAAoB,GAAC,MAA0B,GA4BtD,IAAY,EAAyC,IAAI;SAE/D,EAAa;GAKX,YAA2B,EAAU,OAAO,KAAK,KAAK,QAAQ,QAAQ;GAEtE,aAAa,EAAU,OAAO,MAAM;GAEpC,IAAI,QAAe,EAAU,OAAO,MAAM,IAAI;EAChD,CAAC,mBAIC,EAwBU,GAAA;YAvBJ;GAAJ,KAAI;GACI,MAAM,EAAA;sCAAI,QAAA;GAClB,MAAK;GACJ,OAAO,EAAA;GACP,UAAU,EAAA;GACV,OAAO,EAAA;GACR,cAAA;GACA,uBAAA;GACA,qBAAA;;oBAWQ,CAAR,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA;;;GATQA,EAAAA,OAAO,UAAA;UAAU;WACsB,EADX,sBAAY,CACtD,EAAqD,EAAA,QAAA,WAAA,EAAhB,gBAAY,CAAA,CAAA,CAAA;;;GAEnCA,EAAAA,OAAO,SAAA;UAAS;gBACR,CAAtB,EAAsB,EAAA,QAAA,QAAA,CAAA,CAAA;;;GAERA,EAAAA,OAAM,oBAAA;UAAqB;gBACX,CAA9B,EAA8B,EAAA,QAAA,gBAAA,CAAA,CAAA;;;GAGhBA,EAAAA,OAAO,SAAA;UAAS;gBACR,CAAtB,EAAsB,EAAA,QAAA,QAAA,CAAA,CAAA"}
|
|
@@ -32,13 +32,7 @@ export type FileInputSize = 'sm' | 'md' | 'lg';
|
|
|
32
32
|
interface FileInputProps {
|
|
33
33
|
/** Allows several files to be chosen. With one only, every extra file is turned away. */
|
|
34
34
|
multiple?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`).
|
|
37
|
-
*
|
|
38
|
-
* It is applied TWICE, and it has to be: as an attribute, which is what filters the
|
|
39
|
-
* system's file dialog, and again in code, which is the only thing that can filter a
|
|
40
|
-
* file DROPPED on the component, the attribute having no say over a drop.
|
|
41
|
-
*/
|
|
35
|
+
/** Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`). */
|
|
42
36
|
accept?: string;
|
|
43
37
|
/**
|
|
44
38
|
* How the chosen files are shown: their names joined by commas, or one dismissible
|
|
@@ -86,10 +80,9 @@ interface FileInputProps {
|
|
|
86
80
|
*/
|
|
87
81
|
placeholder?: string;
|
|
88
82
|
/**
|
|
89
|
-
* An icon inside the field, at the start.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* needs `iconStartLabel`.
|
|
83
|
+
* An icon inside the field, at the start. Decorative by default, it becomes a real button as
|
|
84
|
+
* soon as a `@click:icon-start` listener is attached, in which case it needs
|
|
85
|
+
* `iconStartLabel`.
|
|
93
86
|
*/
|
|
94
87
|
iconStart?: IconSource;
|
|
95
88
|
/** What the start icon does, in words, once it is clickable. */
|
|
@@ -100,9 +93,8 @@ interface FileInputProps {
|
|
|
100
93
|
*/
|
|
101
94
|
pickerIconLabel?: string;
|
|
102
95
|
/**
|
|
103
|
-
* Shows a spinner at the end of the field, in place of the attach icon, while an
|
|
104
|
-
*
|
|
105
|
-
* nothing else: files can still be dropped and the dialog still opens.
|
|
96
|
+
* Shows a spinner at the end of the field, in place of the attach icon, while an upload is
|
|
97
|
+
* under way, typically.
|
|
106
98
|
*/
|
|
107
99
|
loading?: boolean;
|
|
108
100
|
/**
|
|
@@ -110,11 +102,7 @@ interface FileInputProps {
|
|
|
110
102
|
* system dictionary.
|
|
111
103
|
*/
|
|
112
104
|
loadingText?: string;
|
|
113
|
-
/**
|
|
114
|
-
* Offers a cross that empties the selection. Worth turning on here more than on an
|
|
115
|
-
* ordinary field: what a picker holds cannot be erased by typing, so the cross is the
|
|
116
|
-
* only way back out of a wrong choice.
|
|
117
|
-
*/
|
|
105
|
+
/** Offers a cross that empties the selection. */
|
|
118
106
|
clearable?: boolean;
|
|
119
107
|
/** What that cross does, in words. It falls back to the design system dictionary. */
|
|
120
108
|
clearLabel?: string;
|
|
@@ -127,20 +115,11 @@ type __VLS_Slots = {
|
|
|
127
115
|
*/
|
|
128
116
|
start?(): unknown;
|
|
129
117
|
/**
|
|
130
|
-
* Controls of your own inside the field, placed before the ones the field owns: the
|
|
131
|
-
*
|
|
132
|
-
* own affordance, which is why there is no `#end` here: it would replace them.
|
|
118
|
+
* Controls of your own inside the field, placed before the ones the field owns: the clear
|
|
119
|
+
* cross and the icon that opens the file dialog.
|
|
133
120
|
*/
|
|
134
121
|
'value-end'?(): unknown;
|
|
135
|
-
/**
|
|
136
|
-
* Replaces the chip standing for one file.
|
|
137
|
-
*
|
|
138
|
-
* Three of the values it receives are what make it usable without regressions:
|
|
139
|
-
* `label` is the name already shortened in the MIDDLE so that its extension survives,
|
|
140
|
-
* `remove` is what takes the file out (without it the file could no longer be removed
|
|
141
|
-
* at all), and the size and density are the ones worked out to sit inside the field,
|
|
142
|
-
* which cannot be guessed from outside.
|
|
143
|
-
*/
|
|
122
|
+
/** Replaces the chip standing for one file. */
|
|
144
123
|
chip?(props: FileInputChipSlotProps): unknown;
|
|
145
124
|
/**
|
|
146
125
|
* Replaces the counter under the field. `text` is the sentence already built and
|
|
@@ -149,12 +128,7 @@ type __VLS_Slots = {
|
|
|
149
128
|
counter?(props: FileInputCounterSlotProps): unknown;
|
|
150
129
|
};
|
|
151
130
|
type __VLS_ModelProps = {
|
|
152
|
-
/**
|
|
153
|
-
* Always a LIST of files, whether or not several are allowed, and never a file on its own.
|
|
154
|
-
* The shape of the value does not depend on a prop, so a consumer never has to narrow a
|
|
155
|
-
* union TypeScript has no way of discriminating. With a single file it is simply a list
|
|
156
|
-
* of at most one.
|
|
157
|
-
*/
|
|
131
|
+
/** Always a LIST of files, whether or not several are allowed, and never a file on its own. */
|
|
158
132
|
modelValue?: File[];
|
|
159
133
|
};
|
|
160
134
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VFileInput.js","names":[],"sources":["../../../src/components/VFileInput/VFileInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * File selection presented as an ordinary form field: a read-only VInput showing what was\n * picked, with the real file input hidden behind it — VDateInput's `readonly` shape, minus\n * the panel. The v-model is always a `File[]`, `multiple` or not, so a consumer never has to\n * narrow a union TypeScript cannot discriminate.\n *\n * The JS is imposed by the platform, not chosen. A dialog opens ONLY from a real click on a\n * file input, and a `FileList` cannot be written from a template — so the visible field can\n * never BE the native control, and that control is only ever a SOURCE of files.\n *\n * The rest follows: the relayed click, the reset after every change (without which the same\n * file cannot be picked twice running), and screening in code, `accept` having no say over\n * a drop.\n */\nimport { computed, inject, ref, useId } from 'vue'\n\nimport VChip from '../VChip/VChip.vue'\nimport type { ChipSize } from '../VChip/VChip.vue'\nimport { attach_file as attachFileIcon } from '../VIcon/icons/attach_file'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { canClear } from '../../composables/useClearable'\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { chipScaleFor } from '../../utils/chip'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { joinIds } from '../../utils/ids'\nimport { truncateMiddle } from './truncate'\n\n/** How the chosen files are shown inside the field when several are allowed. */\nexport type FileInputDisplay = 'chip' | 'text'\n\n/** What the `#chip` slot receives. */\nexport interface FileInputChipSlotProps {\n /** The file the chip stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Its name, already shortened in the middle so the extension survives. */\n label: string\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The chip size worked out to sit inside the field, one step below it. */\n size: ChipSize\n /** Whether the chip takes its compact height, to catch up below the smallest step. */\n compact: boolean\n}\n\n/** What the `#counter` slot receives. */\nexport interface FileInputCounterSlotProps {\n /** How many files are selected. */\n count: number\n /** Their total size, in bytes. */\n bytes: number\n /** The sentence the counter shows by default, already translated. */\n text: string\n}\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type FileInputSize = 'sm' | 'md' | 'lg'\n\ninterface FileInputProps {\n /** Allows several files to be chosen. With one only, every extra file is turned away. */\n multiple?: boolean\n /**\n * Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`).\n *\n * It is applied TWICE, and it has to be: as an attribute, which is what filters the\n * system's file dialog, and again in code, which is the only thing that can filter a\n * file DROPPED on the component, the attribute having no say over a drop.\n */\n accept?: string\n /**\n * How the chosen files are shown: their names joined by commas, or one dismissible\n * chip each. It only means something when several files are allowed; a single name is\n * always text.\n */\n display?: FileInputDisplay\n /** The largest ONE file may be, in bytes. */\n maxSize?: number\n /** The largest the whole selection may be, in bytes. */\n maxTotalSize?: number\n /** How many files may be chosen at most. */\n maxFiles?: number\n /** Shows how much has been chosen under the field: \"3 files (1.2 MB)\". */\n counter?: boolean\n /** The icon at the end of the field, which opens the file dialog. */\n pickerIcon?: IconSource\n /** Refuses files dropped onto the component: only the dialog then adds any. */\n noDrop?: boolean\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: FileInputSize\n /** Takes 4px off the height, leaving the padding, the text and the icons as they are. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was chosen without allowing it to change: no dialog, no drop, no removal.\n */\n readonly?: boolean\n /**\n * Marks the field as invalid, for a rule of your own, since nothing here is checked\n * by the browser.\n */\n invalid?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, to the left of the counter. It is tied to the field\n * for assistive technology.\n */\n hint?: string\n /**\n * What the field says while nothing is chosen. It falls back to the design system\n * dictionary.\n */\n placeholder?: string\n /**\n * An icon inside the field, at the start. It is rendered before the chips rather than\n * in their place, so it survives a chip display. Decorative by default, it becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case it\n * needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the button at the end of the field does, in words. It names the button\n * `pickerIcon` renders, and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the attach icon, while an\n * upload is under way, typically. It says that something is happening and changes\n * nothing else: files can still be dropped and the dialog still opens.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /**\n * Offers a cross that empties the selection. Worth turning on here more than on an\n * ordinary field: what a picker holds cannot be erased by typing, so the cross is the\n * only way back out of a wrong choice.\n */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FileInputProps>(), {\n multiple: false,\n accept: undefined,\n display: 'text',\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n counter: false,\n pickerIcon: () => attachFileIcon,\n noDrop: false,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection: added something, removed something, cleared it. */\n change: [files: File[]]\n /**\n * A file was turned away and never joined the selection. It is emitted once PER file,\n * so a batch drop can be reported precisely.\n */\n reject: [rejection: FileRejection]\n /**\n * ONE file was taken out through its chip, with the file and the position it held.\n * `change` follows it with the whole list, as it does after every other change.\n */\n remove: [file: File, index: number]\n /** The clear cross emptied the selection. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * files rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the\n * clear cross and the icon that opens the file dialog. Those two are this component's\n * own affordance, which is why there is no `#end` here: it would replace them.\n */\n 'value-end'?(): unknown\n /**\n * Replaces the chip standing for one file.\n *\n * Three of the values it receives are what make it usable without regressions:\n * `label` is the name already shortened in the MIDDLE so that its extension survives,\n * `remove` is what takes the file out (without it the file could no longer be removed\n * at all), and the size and density are the ones worked out to sit inside the field,\n * which cannot be guessed from outside.\n */\n chip?(props: FileInputChipSlotProps): unknown\n /**\n * Replaces the counter under the field. `text` is the sentence already built and\n * translated; the count and the total size in `bytes` are there for a wording of your own.\n */\n counter?(props: FileInputCounterSlotProps): unknown\n}>()\n\n// `get` reads an explicit `null` as the empty list: the default applies only to a model\n// that was never given, and `files = ref(null)` is a common start in untyped code.\n/**\n * Always a LIST of files, whether or not several are allowed, and never a file on its own.\n * The shape of the value does not depend on a prop, so a consumer never has to narrow a\n * union TypeScript has no way of discriminating. With a single file it is simply a list\n * of at most one.\n */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand — and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\nconst m = useMessages()\nconst locale = useLocale()\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). The chips read the resolved pair\n// too, or they would come out one step off the field they sit in.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n/*\n * The hidden input, the sorting of the consumer's attributes, the single entry point into\n * the value, the removal of a file and the emptying of all of them live in `useFileField`,\n * shared with VFilePicker.\n *\n * Here, the bucket meant for \"the control the user deals with\" goes to the visible\n * field: that is what they see, focus and click, so a consumer's own `<label for>` has\n * to point at it. The description attribute is pulled out of that bucket, because this\n * component re-assembles it further down.\n */\nconst {\n fileEl,\n enabled,\n nativeInputAttrs,\n controlAttrs,\n acceptFiles,\n openPicker,\n removeAt,\n clear,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n disabled: resolvedDisabled,\n excludeFromControl: ['aria-describedby'],\n onClear: () => emit('clear'),\n // Behind `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n props.display === 'chip' &&\n !props.multiple &&\n 'display=\"chip\" ignored without `multiple`: a single file shows as text.',\n ]\n : undefined,\n})\n\n/** What reaches the visible field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...controlAttrs.value, ...iconStartClick }))\n\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Chips only mean something for a list: a single file always shows as plain text.\nconst resolvedDisplay = computed<FileInputDisplay>(() => (props.multiple ? props.display : 'text'))\n\nconst displayText = computed(() =>\n resolvedDisplay.value === 'chip' ? '' : model.value.map((file) => file.name).join(', '),\n)\n\nconst placeholderText = computed(() =>\n model.value.length > 0 ? undefined : (props.placeholder ?? m.value.fileInput.placeholder),\n)\n\n/*\n * Whether the clear cross is shown has to be answered EXPLICITLY here. The visible field\n * is read-only, and a read-only field hides its cross by default — rightly so, since its\n * text cannot be edited. Here the value comes from the file dialog rather than from\n * typing, so there is something to clear all the same.\n */\nconst clearVisible = computed(() => canClear(props, resolvedDisabled.value, model.value.length > 0))\n\n// @a11y @devwarn\n/*\n * The icon disappears along with what it offers: a read-only field opens no dialog, so\n * an icon inviting one would be a lie — the same rule VDateInput follows for its\n * calendar.\n *\n * TRAP — its LABEL stays defined at all times, even when no icon is rendered. The helper\n * that detects a click handler on an icon warns AT SETUP if one is attached without a\n * label, and it has no way of knowing whether an icon exists.\n */\nconst endIcon = computed<IconSource | undefined>(() =>\n props.readonly ? undefined : props.pickerIcon,\n)\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.fileInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.fileInput.clear)\n\nconst totalSize = computed(() => model.value.reduce((sum, file) => sum + file.size, 0))\n\n/*\n * The counter is assembled from three sources: the WORD comes from the dictionary, since\n * it is language; the SIZE is formatted by the platform, which knows the local\n * conventions; and the parentheses come from neither, being punctuation every language\n * shares — the same boundary VBadge's \"99+\" falls on.\n *\n * With nothing chosen it states the count alone: \"0 files ()\" would be noise.\n */\nconst counterText = computed(() => {\n const word = m.value.fileInput.files(model.value.length)\n return model.value.length === 0 ? word : `${word} (${formatBytes(totalSize.value, locale.value)})`\n})\n\nconst hintId = useId()\nconst counterId = useId()\n\n// @a11y\n/*\n * What describes the field for a screen reader is a LIST of references, assembled here\n * rather than by VInput: the hint belongs to OUR row under the field, VInput having no\n * such row — its own counter sits inside the field.\n *\n * The counter is deliberately part of that list, unlike in VTextarea. When the files are\n * shown as chips the field's own text is EMPTY, and the counter is then the only spoken\n * summary of what has been chosen.\n */\nconst describedBy = computed(() =>\n joinIds(\n attrs['aria-describedby'] as string | undefined,\n !!props.hint && hintId,\n props.counter && counterId,\n ),\n)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VCombobox. The height is set inline rather than\n// restated as a table of CSS rules: it belongs to the chips' own subtree, out of the\n// field's reach, and the field has to force its input to that same height or it grows\n// when focused.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/**\n * What each chip shows for its file, worked out once per change of the selection rather\n * than on every render, which reads it three times per chip. The name is shortened in the\n * MIDDLE rather than cut off at the end, for two reasons.\n *\n * The chips WRAP, so there is no line for a long name to overflow: left whole it would\n * simply push the field onto two or three rows. And cutting the middle preserves the\n * extension, which is what tells the reader what kind of file it is — an ellipsis at the\n * end drops it first.\n *\n * The full name is never lost: the removal button is named with it, and the chip carries\n * it as a tooltip whenever the label was actually shortened.\n */\n// Computed lazily, so a text display, which never reads it, never pays for it.\nconst chipLabels = computed(() => model.value.map((file) => truncateMiddle(file.name)))\n\nfunction onControlClick(event: MouseEvent) {\n // The field's own buttons already handle their clicks, and the click also reaches the\n // field itself. Without this guard the attach icon would open the dialog TWICE, and\n // the clear cross would reopen it immediately after emptying the selection.\n if ((event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return\n // Cancelling the default on Enter also stops the form from being submitted: a\n // read-only text field is still a field, and Enter in one submits the form around it.\n event.preventDefault()\n openPicker()\n}\n\nfunction removeFile(index: number) {\n // The removal button disappears along with its chip, so the focus would fall back to\n // the page body. Unlike the date and time pickers, nothing here opens on focus, so\n // this needs no guard against re-entering.\n if (removeAt(index)) inputRef.value?.focus()\n}\n\n/* Files may be dropped on the component itself; there is no separate drop area here —\n that is what VFilePicker is for. */\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => !props.noDrop && enabled.value,\n acceptFiles,\n)\n\ndefineExpose({\n /** Moves the focus to the visible field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did (a\n * click, a key press): browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The component's own box, for what neither of the two above covers. It is the wrapper\n * and not the hidden file input: that one is the FORM's element, this is the reader's.\n */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-file-input\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-display=\"resolvedDisplay\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-dragging=\"dragging ? '' : undefined\"\n @dragenter=\"onDragEnter\"\n @dragover=\"onDragOver\"\n @dragleave=\"onDragLeave\"\n @drop=\"onDrop\"\n >\n <input v-bind=\"nativeInputAttrs\" ref=\"fileEl\" class=\"v-file-input-native v-hidden-input\" />\n\n <div class=\"v-file-input-control\" @click=\"onControlClick\">\n <!-- Shown as chips, the field takes VInput's two arrangements for chips: its end\n controls lifted out of the flow, and the chips wrapping onto several rows. -->\n <VInput\n v-bind=\"fieldAttrs\"\n ref=\"inputRef\"\n :class=\"{ 'v-input-end-pinned v-input-chips': resolvedDisplay === 'chip' }\"\n :model-value=\"displayText\"\n no-typing\n :readonly=\"readonly\"\n :label=\"label\"\n :placeholder=\"placeholderText\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n :aria-describedby=\"describedBy\"\n @click:icon-end=\"openPicker\"\n @clear=\"clear\"\n @keydown=\"onFieldKeydown\"\n >\n <template v-if=\"resolvedDisplay === 'chip' || $slots.start\" #start>\n <template\n v-for=\"(file, index) in resolvedDisplay === 'chip' ? model : []\"\n :key=\"fileKey(file)\"\n >\n <slot\n name=\"chip\"\n :file=\"file\"\n :index=\"index\"\n :label=\"chipLabels[index]!\"\n :remove=\"() => removeFile(index)\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n >\n <VChip\n tone=\"accent\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n :dismissible=\"enabled\"\n :dismiss-label=\"m.common.remove(file.name)\"\n :disabled=\"resolvedDisabled\"\n :title=\"chipLabels[index] === file.name ? undefined : file.name\"\n @dismiss=\"removeFile(index)\"\n >{{ chipLabels[index] }}</VChip\n >\n </slot>\n </template>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n </VInput>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-file-input-meta v-field-meta\">\n <VTypography\n v-if=\"hint\"\n :id=\"hintId\"\n variant=\"caption\"\n tone=\"muted\"\n class=\"v-file-input-hint\"\n >\n {{ hint }}\n </VTypography>\n <span v-if=\"counter\" :id=\"counterId\" class=\"v-file-input-counter v-field-counter\">\n <slot name=\"counter\" :count=\"model.length\" :bytes=\"totalSize\" :text=\"counterText\">\n {{ counterText }}\n </slot>\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* Positioned so the hidden file input, which has no size at all, has something to be\n placed against. */\n .v-file-input {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /* The real file input is a SOURCE of files and not a control anyone deals with: it\n wears `.v-hidden-input`, and taking it out of the tab order and hiding it from screen\n readers, in `useFileField`, is what leaves the visible field as the single stop and the\n single announcement. */\n .v-file-input:not([data-disabled]):not([data-readonly]) .v-file-input-control,\n .v-file-input:not([data-disabled]):not([data-readonly])[data-display='text'] .v-input-control {\n cursor: pointer;\n }\n\n /* An ellipsis DOES work on an input, and a read-only one never scrolls by itself, so a\n long list of names is simply cut short. No tooltip is added for the rest: it would be\n announced as a description repeating the value the field already shows, and the\n complete list is what the model is for. */\n .v-file-input[data-display='text'] .v-input-control {\n text-overflow: ellipsis;\n }\n\n /* While a file is being dragged over it, the field is highlighted by redefining the\n very variable VInput uses for its own border colour — so the two can never disagree.\n\n TRAP — the selector weighs (0,4,0), one step above VInput's states, and it has to. The\n field inside is ALWAYS read-only, and VInput's read-only background weighs (0,3,0): at\n that weight the winner would be whichever of the two sheets a bundler put last. */\n .v-file-input[data-dragging] .v-input .v-input-field {\n --field-border-color: var(--vectis-color-accent);\n\n background: var(--vectis-color-accent-surface);\n }\n\n .v-file-input[data-disabled] .v-file-input-hint,\n .v-file-input[data-disabled] .v-file-input-counter {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VFileInput.js","names":[],"sources":["../../../src/components/VFileInput/VFileInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * Compose a read-only VInput with a native file input. useFileField bridges File[] selection to\n * the browser dialog and form submission.\n */\nimport { computed, inject, ref, useId } from 'vue'\n\nimport VChip from '../VChip/VChip.vue'\nimport type { ChipSize } from '../VChip/VChip.vue'\nimport { attach_file as attachFileIcon } from '../VIcon/icons/attach_file'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { canClear } from '../../composables/useClearable'\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { chipScaleFor } from '../../utils/chip'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { joinIds } from '../../utils/ids'\nimport { truncateMiddle } from './truncate'\n\n/** How the chosen files are shown inside the field when several are allowed. */\nexport type FileInputDisplay = 'chip' | 'text'\n\n/** What the `#chip` slot receives. */\nexport interface FileInputChipSlotProps {\n /** The file the chip stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Its name, already shortened in the middle so the extension survives. */\n label: string\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The chip size worked out to sit inside the field, one step below it. */\n size: ChipSize\n /** Whether the chip takes its compact height, to catch up below the smallest step. */\n compact: boolean\n}\n\n/** What the `#counter` slot receives. */\nexport interface FileInputCounterSlotProps {\n /** How many files are selected. */\n count: number\n /** Their total size, in bytes. */\n bytes: number\n /** The sentence the counter shows by default, already translated. */\n text: string\n}\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type FileInputSize = 'sm' | 'md' | 'lg'\n\ninterface FileInputProps {\n /** Allows several files to be chosen. With one only, every extra file is turned away. */\n multiple?: boolean\n /** Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`). */\n accept?: string\n /**\n * How the chosen files are shown: their names joined by commas, or one dismissible\n * chip each. It only means something when several files are allowed; a single name is\n * always text.\n */\n display?: FileInputDisplay\n /** The largest ONE file may be, in bytes. */\n maxSize?: number\n /** The largest the whole selection may be, in bytes. */\n maxTotalSize?: number\n /** How many files may be chosen at most. */\n maxFiles?: number\n /** Shows how much has been chosen under the field: \"3 files (1.2 MB)\". */\n counter?: boolean\n /** The icon at the end of the field, which opens the file dialog. */\n pickerIcon?: IconSource\n /** Refuses files dropped onto the component: only the dialog then adds any. */\n noDrop?: boolean\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: FileInputSize\n /** Takes 4px off the height, leaving the padding, the text and the icons as they are. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was chosen without allowing it to change: no dialog, no drop, no removal.\n */\n readonly?: boolean\n /**\n * Marks the field as invalid, for a rule of your own, since nothing here is checked\n * by the browser.\n */\n invalid?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, to the left of the counter. It is tied to the field\n * for assistive technology.\n */\n hint?: string\n /**\n * What the field says while nothing is chosen. It falls back to the design system\n * dictionary.\n */\n placeholder?: string\n /**\n * An icon inside the field, at the start. Decorative by default, it becomes a real button as\n * soon as a `@click:icon-start` listener is attached, in which case it needs\n * `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the button at the end of the field does, in words. It names the button\n * `pickerIcon` renders, and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the attach icon, while an upload is\n * under way, typically.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /** Offers a cross that empties the selection. */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FileInputProps>(), {\n multiple: false,\n accept: undefined,\n display: 'text',\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n counter: false,\n pickerIcon: () => attachFileIcon,\n noDrop: false,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection: added something, removed something, cleared it. */\n change: [files: File[]]\n /**\n * A file was turned away and never joined the selection. It is emitted once PER file,\n * so a batch drop can be reported precisely.\n */\n reject: [rejection: FileRejection]\n /**\n * ONE file was taken out through its chip, with the file and the position it held.\n * `change` follows it with the whole list, as it does after every other change.\n */\n remove: [file: File, index: number]\n /** The clear cross emptied the selection. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * files rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the clear\n * cross and the icon that opens the file dialog.\n */\n 'value-end'?(): unknown\n /** Replaces the chip standing for one file. */\n chip?(props: FileInputChipSlotProps): unknown\n /**\n * Replaces the counter under the field. `text` is the sentence already built and\n * translated; the count and the total size in `bytes` are there for a wording of your own.\n */\n counter?(props: FileInputCounterSlotProps): unknown\n}>()\n\n// `get` reads an explicit `null` as the empty list: the default applies only to a model\n// that was never given, and `files = ref(null)` is a common start in untyped code.\n/** Always a LIST of files, whether or not several are allowed, and never a file on its own. */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand; and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\nconst m = useMessages()\nconst locale = useLocale()\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). The chips read the resolved pair\n// too, or they would come out one step off the field they sit in.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n/*\n * Here, the bucket meant for \"the control the user deals with\" goes to the visible field: that\n * is what they see, focus and click, so a consumer's own `<label for>` has to point at it. The\n * description attribute is pulled out of that bucket, because this component re-assembles it\n * further down.\n */\nconst {\n fileEl,\n enabled,\n nativeInputAttrs,\n controlAttrs,\n acceptFiles,\n openPicker,\n removeAt,\n clear,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n disabled: resolvedDisabled,\n excludeFromControl: ['aria-describedby'],\n onClear: () => emit('clear'),\n warnings: isDev\n ? () => [\n props.display === 'chip' &&\n !props.multiple &&\n 'display=\"chip\" ignored without `multiple`: a single file shows as text.',\n ]\n : undefined,\n})\n\n/** What reaches the visible field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...controlAttrs.value, ...iconStartClick }))\n\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst resolvedDisplay = computed<FileInputDisplay>(() => (props.multiple ? props.display : 'text'))\n\nconst displayText = computed(() =>\n resolvedDisplay.value === 'chip' ? '' : model.value.map((file) => file.name).join(', '),\n)\n\nconst placeholderText = computed(() =>\n model.value.length > 0 ? undefined : (props.placeholder ?? m.value.fileInput.placeholder),\n)\n\n/*\n * The visible field is read-only, and a read-only field hides its cross by default; rightly so,\n * since its text cannot be edited. Here the value comes from the file dialog rather than from\n * typing, so there is something to clear all the same.\n */\nconst clearVisible = computed(() => canClear(props, resolvedDisabled.value, model.value.length > 0))\n\n// @a11y @devwarn\n/*\n * Its label stays defined always, even when no icon is rendered. The helper that detects a\n * click handler on an icon warns AT setup if one is attached without a label, and it has no way\n * of knowing whether an icon exists.\n */\nconst endIcon = computed<IconSource | undefined>(() =>\n props.readonly ? undefined : props.pickerIcon,\n)\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.fileInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.fileInput.clear)\n\nconst totalSize = computed(() => model.value.reduce((sum, file) => sum + file.size, 0))\n\n/*\n * The counter is assembled from three sources: the WORD comes from the dictionary, since it is\n * language; the SIZE is formatted by the platform, which knows the local conventions; and the\n * parentheses come from neither, being punctuation every language shares; the same boundary\n * VBadge's \"99+\" falls on. With nothing chosen it states the count alone: \"0 files ()\" would be\n * noise.\n */\nconst counterText = computed(() => {\n const word = m.value.fileInput.files(model.value.length)\n return model.value.length === 0 ? word : `${word} (${formatBytes(totalSize.value, locale.value)})`\n})\n\nconst hintId = useId()\nconst counterId = useId()\n\n// @a11y\n/*\n * What describes the field for a screen reader is a LIST of references, assembled here rather\n * than by VInput: the hint belongs to OUR row under the field, VInput having no such row; its\n * own counter sits inside the field. The counter is deliberately part of that list, unlike in\n * VTextarea.\n */\nconst describedBy = computed(() =>\n joinIds(\n attrs['aria-describedby'] as string | undefined,\n !!props.hint && hintId,\n props.counter && counterId,\n ),\n)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VCombobox. The height is set inline rather than\n// restated as a table of CSS rules: it belongs to the chips' own subtree, out of the\n// field's reach, and the field has to force its input to that same height or it grows\n// when focused.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/**\n * What each chip shows for its file, worked out once per change of the selection rather than on\n * every render, which reads it three times per chip.\n */\n// Computed lazily, so a text display, which never reads it, never pays for it.\nconst chipLabels = computed(() => model.value.map((file) => truncateMiddle(file.name)))\n\nfunction onControlClick(event: MouseEvent) {\n // Without this guard the attach icon would open the dialog TWICE, and the clear cross would\n // reopen it immediately after emptying the selection.\n if ((event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return\n // Cancelling the default on Enter also stops the form from being submitted: a\n // read-only text field is still a field, and Enter in one submits the form around it.\n event.preventDefault()\n openPicker()\n}\n\nfunction removeFile(index: number) {\n // The removal button disappears along with its chip, so the focus would fall back to\n // the page body. Unlike the date and time pickers, nothing here opens on focus, so\n // this needs no guard against re-entering.\n if (removeAt(index)) inputRef.value?.focus()\n}\n\n/*\n * Files may be dropped on the component itself; there is no separate drop area here; that is\n * what VFilePicker is for.\n */\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => !props.noDrop && enabled.value,\n acceptFiles,\n)\n\ndefineExpose({\n /** Moves the focus to the visible field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did (a\n * click, a key press): browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The component's own box, for what neither of the two above covers. It is the wrapper\n * and not the hidden file input: that one is the FORM's element, this is the reader's.\n */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-file-input\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-display=\"resolvedDisplay\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-dragging=\"dragging ? '' : undefined\"\n @dragenter=\"onDragEnter\"\n @dragover=\"onDragOver\"\n @dragleave=\"onDragLeave\"\n @drop=\"onDrop\"\n >\n <input v-bind=\"nativeInputAttrs\" ref=\"fileEl\" class=\"v-file-input-native v-hidden-input\" />\n\n <div class=\"v-file-input-control\" @click=\"onControlClick\">\n <!-- Shown as chips, the field takes VInput's two arrangements for chips: its end\n controls lifted out of the flow, and the chips wrapping onto several rows. -->\n <VInput\n v-bind=\"fieldAttrs\"\n ref=\"inputRef\"\n :class=\"{ 'v-input-end-pinned v-input-chips': resolvedDisplay === 'chip' }\"\n :model-value=\"displayText\"\n no-typing\n :readonly=\"readonly\"\n :label=\"label\"\n :placeholder=\"placeholderText\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n :aria-describedby=\"describedBy\"\n @click:icon-end=\"openPicker\"\n @clear=\"clear\"\n @keydown=\"onFieldKeydown\"\n >\n <template v-if=\"resolvedDisplay === 'chip' || $slots.start\" #start>\n <template\n v-for=\"(file, index) in resolvedDisplay === 'chip' ? model : []\"\n :key=\"fileKey(file)\"\n >\n <slot\n name=\"chip\"\n :file=\"file\"\n :index=\"index\"\n :label=\"chipLabels[index]!\"\n :remove=\"() => removeFile(index)\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n >\n <VChip\n tone=\"accent\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n :dismissible=\"enabled\"\n :dismiss-label=\"m.common.remove(file.name)\"\n :disabled=\"resolvedDisabled\"\n :title=\"chipLabels[index] === file.name ? undefined : file.name\"\n @dismiss=\"removeFile(index)\"\n >{{ chipLabels[index] }}</VChip\n >\n </slot>\n </template>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n </VInput>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-file-input-meta v-field-meta\">\n <VTypography\n v-if=\"hint\"\n :id=\"hintId\"\n variant=\"caption\"\n tone=\"muted\"\n class=\"v-file-input-hint\"\n >\n {{ hint }}\n </VTypography>\n <span v-if=\"counter\" :id=\"counterId\" class=\"v-file-input-counter v-field-counter\">\n <slot name=\"counter\" :count=\"model.length\" :bytes=\"totalSize\" :text=\"counterText\">\n {{ counterText }}\n </slot>\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-file-input {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The real file input is a source of files and not a control anyone deals with: it wears\n * `.v-hidden-input`, and taking it out of the tab order and hiding it from screen readers, in\n * `useFileField`, is what leaves the visible field as the single stop and the single\n * announcement.\n */\n .v-file-input:not([data-disabled]):not([data-readonly]) .v-file-input-control,\n .v-file-input:not([data-disabled]):not([data-readonly])[data-display='text'] .v-input-control {\n cursor: pointer;\n }\n\n /* An ellipsis DOES work on an input, and a read-only one never scrolls by itself, so a\n long list of names is simply cut short. No tooltip is added for the rest: it would be\n announced as a description repeating the value the field already shows, and the\n complete list is what the model is for. */\n .v-file-input[data-display='text'] .v-input-control {\n text-overflow: ellipsis;\n }\n\n /*\n * The selector weighs (0,4,0), one step above VInput's states, and it has to. The field\n * inside is always read-only, and VInput's read-only background weighs (0,3,0): at that\n * weight the winner would be whichever of the two sheets a bundler put last.\n */\n .v-file-input[data-dragging] .v-input .v-input-field {\n --field-border-color: var(--vectis-color-accent);\n\n background: var(--vectis-color-accent-surface);\n }\n\n .v-file-input[data-disabled] .v-file-input-hint,\n .v-file-input[data-disabled] .v-file-input-counter {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VFileInput.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VFileInput/VFileInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * File selection presented as an ordinary form field: a read-only VInput showing what was\n * picked, with the real file input hidden behind it — VDateInput's `readonly` shape, minus\n * the panel. The v-model is always a `File[]`, `multiple` or not, so a consumer never has to\n * narrow a union TypeScript cannot discriminate.\n *\n * The JS is imposed by the platform, not chosen. A dialog opens ONLY from a real click on a\n * file input, and a `FileList` cannot be written from a template — so the visible field can\n * never BE the native control, and that control is only ever a SOURCE of files.\n *\n * The rest follows: the relayed click, the reset after every change (without which the same\n * file cannot be picked twice running), and screening in code, `accept` having no say over\n * a drop.\n */\nimport { computed, inject, ref, useId } from 'vue'\n\nimport VChip from '../VChip/VChip.vue'\nimport type { ChipSize } from '../VChip/VChip.vue'\nimport { attach_file as attachFileIcon } from '../VIcon/icons/attach_file'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { canClear } from '../../composables/useClearable'\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { chipScaleFor } from '../../utils/chip'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { joinIds } from '../../utils/ids'\nimport { truncateMiddle } from './truncate'\n\n/** How the chosen files are shown inside the field when several are allowed. */\nexport type FileInputDisplay = 'chip' | 'text'\n\n/** What the `#chip` slot receives. */\nexport interface FileInputChipSlotProps {\n /** The file the chip stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Its name, already shortened in the middle so the extension survives. */\n label: string\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The chip size worked out to sit inside the field, one step below it. */\n size: ChipSize\n /** Whether the chip takes its compact height, to catch up below the smallest step. */\n compact: boolean\n}\n\n/** What the `#counter` slot receives. */\nexport interface FileInputCounterSlotProps {\n /** How many files are selected. */\n count: number\n /** Their total size, in bytes. */\n bytes: number\n /** The sentence the counter shows by default, already translated. */\n text: string\n}\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type FileInputSize = 'sm' | 'md' | 'lg'\n\ninterface FileInputProps {\n /** Allows several files to be chosen. With one only, every extra file is turned away. */\n multiple?: boolean\n /**\n * Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`).\n *\n * It is applied TWICE, and it has to be: as an attribute, which is what filters the\n * system's file dialog, and again in code, which is the only thing that can filter a\n * file DROPPED on the component, the attribute having no say over a drop.\n */\n accept?: string\n /**\n * How the chosen files are shown: their names joined by commas, or one dismissible\n * chip each. It only means something when several files are allowed; a single name is\n * always text.\n */\n display?: FileInputDisplay\n /** The largest ONE file may be, in bytes. */\n maxSize?: number\n /** The largest the whole selection may be, in bytes. */\n maxTotalSize?: number\n /** How many files may be chosen at most. */\n maxFiles?: number\n /** Shows how much has been chosen under the field: \"3 files (1.2 MB)\". */\n counter?: boolean\n /** The icon at the end of the field, which opens the file dialog. */\n pickerIcon?: IconSource\n /** Refuses files dropped onto the component: only the dialog then adds any. */\n noDrop?: boolean\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: FileInputSize\n /** Takes 4px off the height, leaving the padding, the text and the icons as they are. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was chosen without allowing it to change: no dialog, no drop, no removal.\n */\n readonly?: boolean\n /**\n * Marks the field as invalid, for a rule of your own, since nothing here is checked\n * by the browser.\n */\n invalid?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, to the left of the counter. It is tied to the field\n * for assistive technology.\n */\n hint?: string\n /**\n * What the field says while nothing is chosen. It falls back to the design system\n * dictionary.\n */\n placeholder?: string\n /**\n * An icon inside the field, at the start. It is rendered before the chips rather than\n * in their place, so it survives a chip display. Decorative by default, it becomes a\n * real button as soon as a `@click:icon-start` listener is attached, in which case it\n * needs `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the button at the end of the field does, in words. It names the button\n * `pickerIcon` renders, and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the attach icon, while an\n * upload is under way, typically. It says that something is happening and changes\n * nothing else: files can still be dropped and the dialog still opens.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /**\n * Offers a cross that empties the selection. Worth turning on here more than on an\n * ordinary field: what a picker holds cannot be erased by typing, so the cross is the\n * only way back out of a wrong choice.\n */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FileInputProps>(), {\n multiple: false,\n accept: undefined,\n display: 'text',\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n counter: false,\n pickerIcon: () => attachFileIcon,\n noDrop: false,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection: added something, removed something, cleared it. */\n change: [files: File[]]\n /**\n * A file was turned away and never joined the selection. It is emitted once PER file,\n * so a batch drop can be reported precisely.\n */\n reject: [rejection: FileRejection]\n /**\n * ONE file was taken out through its chip, with the file and the position it held.\n * `change` follows it with the whole list, as it does after every other change.\n */\n remove: [file: File, index: number]\n /** The clear cross emptied the selection. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * files rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the\n * clear cross and the icon that opens the file dialog. Those two are this component's\n * own affordance, which is why there is no `#end` here: it would replace them.\n */\n 'value-end'?(): unknown\n /**\n * Replaces the chip standing for one file.\n *\n * Three of the values it receives are what make it usable without regressions:\n * `label` is the name already shortened in the MIDDLE so that its extension survives,\n * `remove` is what takes the file out (without it the file could no longer be removed\n * at all), and the size and density are the ones worked out to sit inside the field,\n * which cannot be guessed from outside.\n */\n chip?(props: FileInputChipSlotProps): unknown\n /**\n * Replaces the counter under the field. `text` is the sentence already built and\n * translated; the count and the total size in `bytes` are there for a wording of your own.\n */\n counter?(props: FileInputCounterSlotProps): unknown\n}>()\n\n// `get` reads an explicit `null` as the empty list: the default applies only to a model\n// that was never given, and `files = ref(null)` is a common start in untyped code.\n/**\n * Always a LIST of files, whether or not several are allowed, and never a file on its own.\n * The shape of the value does not depend on a prop, so a consumer never has to narrow a\n * union TypeScript has no way of discriminating. With a single file it is simply a list\n * of at most one.\n */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand — and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\nconst m = useMessages()\nconst locale = useLocale()\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). The chips read the resolved pair\n// too, or they would come out one step off the field they sit in.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n/*\n * The hidden input, the sorting of the consumer's attributes, the single entry point into\n * the value, the removal of a file and the emptying of all of them live in `useFileField`,\n * shared with VFilePicker.\n *\n * Here, the bucket meant for \"the control the user deals with\" goes to the visible\n * field: that is what they see, focus and click, so a consumer's own `<label for>` has\n * to point at it. The description attribute is pulled out of that bucket, because this\n * component re-assembles it further down.\n */\nconst {\n fileEl,\n enabled,\n nativeInputAttrs,\n controlAttrs,\n acceptFiles,\n openPicker,\n removeAt,\n clear,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n disabled: resolvedDisabled,\n excludeFromControl: ['aria-describedby'],\n onClear: () => emit('clear'),\n // Behind `isDev`, which a production build folds to false, so the message is dropped.\n warnings: isDev\n ? () => [\n props.display === 'chip' &&\n !props.multiple &&\n 'display=\"chip\" ignored without `multiple`: a single file shows as text.',\n ]\n : undefined,\n})\n\n/** What reaches the visible field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...controlAttrs.value, ...iconStartClick }))\n\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Chips only mean something for a list: a single file always shows as plain text.\nconst resolvedDisplay = computed<FileInputDisplay>(() => (props.multiple ? props.display : 'text'))\n\nconst displayText = computed(() =>\n resolvedDisplay.value === 'chip' ? '' : model.value.map((file) => file.name).join(', '),\n)\n\nconst placeholderText = computed(() =>\n model.value.length > 0 ? undefined : (props.placeholder ?? m.value.fileInput.placeholder),\n)\n\n/*\n * Whether the clear cross is shown has to be answered EXPLICITLY here. The visible field\n * is read-only, and a read-only field hides its cross by default — rightly so, since its\n * text cannot be edited. Here the value comes from the file dialog rather than from\n * typing, so there is something to clear all the same.\n */\nconst clearVisible = computed(() => canClear(props, resolvedDisabled.value, model.value.length > 0))\n\n// @a11y @devwarn\n/*\n * The icon disappears along with what it offers: a read-only field opens no dialog, so\n * an icon inviting one would be a lie — the same rule VDateInput follows for its\n * calendar.\n *\n * TRAP — its LABEL stays defined at all times, even when no icon is rendered. The helper\n * that detects a click handler on an icon warns AT SETUP if one is attached without a\n * label, and it has no way of knowing whether an icon exists.\n */\nconst endIcon = computed<IconSource | undefined>(() =>\n props.readonly ? undefined : props.pickerIcon,\n)\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.fileInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.fileInput.clear)\n\nconst totalSize = computed(() => model.value.reduce((sum, file) => sum + file.size, 0))\n\n/*\n * The counter is assembled from three sources: the WORD comes from the dictionary, since\n * it is language; the SIZE is formatted by the platform, which knows the local\n * conventions; and the parentheses come from neither, being punctuation every language\n * shares — the same boundary VBadge's \"99+\" falls on.\n *\n * With nothing chosen it states the count alone: \"0 files ()\" would be noise.\n */\nconst counterText = computed(() => {\n const word = m.value.fileInput.files(model.value.length)\n return model.value.length === 0 ? word : `${word} (${formatBytes(totalSize.value, locale.value)})`\n})\n\nconst hintId = useId()\nconst counterId = useId()\n\n// @a11y\n/*\n * What describes the field for a screen reader is a LIST of references, assembled here\n * rather than by VInput: the hint belongs to OUR row under the field, VInput having no\n * such row — its own counter sits inside the field.\n *\n * The counter is deliberately part of that list, unlike in VTextarea. When the files are\n * shown as chips the field's own text is EMPTY, and the counter is then the only spoken\n * summary of what has been chosen.\n */\nconst describedBy = computed(() =>\n joinIds(\n attrs['aria-describedby'] as string | undefined,\n !!props.hint && hintId,\n props.counter && counterId,\n ),\n)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VCombobox. The height is set inline rather than\n// restated as a table of CSS rules: it belongs to the chips' own subtree, out of the\n// field's reach, and the field has to force its input to that same height or it grows\n// when focused.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/**\n * What each chip shows for its file, worked out once per change of the selection rather\n * than on every render, which reads it three times per chip. The name is shortened in the\n * MIDDLE rather than cut off at the end, for two reasons.\n *\n * The chips WRAP, so there is no line for a long name to overflow: left whole it would\n * simply push the field onto two or three rows. And cutting the middle preserves the\n * extension, which is what tells the reader what kind of file it is — an ellipsis at the\n * end drops it first.\n *\n * The full name is never lost: the removal button is named with it, and the chip carries\n * it as a tooltip whenever the label was actually shortened.\n */\n// Computed lazily, so a text display, which never reads it, never pays for it.\nconst chipLabels = computed(() => model.value.map((file) => truncateMiddle(file.name)))\n\nfunction onControlClick(event: MouseEvent) {\n // The field's own buttons already handle their clicks, and the click also reaches the\n // field itself. Without this guard the attach icon would open the dialog TWICE, and\n // the clear cross would reopen it immediately after emptying the selection.\n if ((event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return\n // Cancelling the default on Enter also stops the form from being submitted: a\n // read-only text field is still a field, and Enter in one submits the form around it.\n event.preventDefault()\n openPicker()\n}\n\nfunction removeFile(index: number) {\n // The removal button disappears along with its chip, so the focus would fall back to\n // the page body. Unlike the date and time pickers, nothing here opens on focus, so\n // this needs no guard against re-entering.\n if (removeAt(index)) inputRef.value?.focus()\n}\n\n/* Files may be dropped on the component itself; there is no separate drop area here —\n that is what VFilePicker is for. */\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => !props.noDrop && enabled.value,\n acceptFiles,\n)\n\ndefineExpose({\n /** Moves the focus to the visible field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did (a\n * click, a key press): browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The component's own box, for what neither of the two above covers. It is the wrapper\n * and not the hidden file input: that one is the FORM's element, this is the reader's.\n */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-file-input\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-display=\"resolvedDisplay\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-dragging=\"dragging ? '' : undefined\"\n @dragenter=\"onDragEnter\"\n @dragover=\"onDragOver\"\n @dragleave=\"onDragLeave\"\n @drop=\"onDrop\"\n >\n <input v-bind=\"nativeInputAttrs\" ref=\"fileEl\" class=\"v-file-input-native v-hidden-input\" />\n\n <div class=\"v-file-input-control\" @click=\"onControlClick\">\n <!-- Shown as chips, the field takes VInput's two arrangements for chips: its end\n controls lifted out of the flow, and the chips wrapping onto several rows. -->\n <VInput\n v-bind=\"fieldAttrs\"\n ref=\"inputRef\"\n :class=\"{ 'v-input-end-pinned v-input-chips': resolvedDisplay === 'chip' }\"\n :model-value=\"displayText\"\n no-typing\n :readonly=\"readonly\"\n :label=\"label\"\n :placeholder=\"placeholderText\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n :aria-describedby=\"describedBy\"\n @click:icon-end=\"openPicker\"\n @clear=\"clear\"\n @keydown=\"onFieldKeydown\"\n >\n <template v-if=\"resolvedDisplay === 'chip' || $slots.start\" #start>\n <template\n v-for=\"(file, index) in resolvedDisplay === 'chip' ? model : []\"\n :key=\"fileKey(file)\"\n >\n <slot\n name=\"chip\"\n :file=\"file\"\n :index=\"index\"\n :label=\"chipLabels[index]!\"\n :remove=\"() => removeFile(index)\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n >\n <VChip\n tone=\"accent\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n :dismissible=\"enabled\"\n :dismiss-label=\"m.common.remove(file.name)\"\n :disabled=\"resolvedDisabled\"\n :title=\"chipLabels[index] === file.name ? undefined : file.name\"\n @dismiss=\"removeFile(index)\"\n >{{ chipLabels[index] }}</VChip\n >\n </slot>\n </template>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n </VInput>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-file-input-meta v-field-meta\">\n <VTypography\n v-if=\"hint\"\n :id=\"hintId\"\n variant=\"caption\"\n tone=\"muted\"\n class=\"v-file-input-hint\"\n >\n {{ hint }}\n </VTypography>\n <span v-if=\"counter\" :id=\"counterId\" class=\"v-file-input-counter v-field-counter\">\n <slot name=\"counter\" :count=\"model.length\" :bytes=\"totalSize\" :text=\"counterText\">\n {{ counterText }}\n </slot>\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* Positioned so the hidden file input, which has no size at all, has something to be\n placed against. */\n .v-file-input {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /* The real file input is a SOURCE of files and not a control anyone deals with: it\n wears `.v-hidden-input`, and taking it out of the tab order and hiding it from screen\n readers, in `useFileField`, is what leaves the visible field as the single stop and the\n single announcement. */\n .v-file-input:not([data-disabled]):not([data-readonly]) .v-file-input-control,\n .v-file-input:not([data-disabled]):not([data-readonly])[data-display='text'] .v-input-control {\n cursor: pointer;\n }\n\n /* An ellipsis DOES work on an input, and a read-only one never scrolls by itself, so a\n long list of names is simply cut short. No tooltip is added for the rest: it would be\n announced as a description repeating the value the field already shows, and the\n complete list is what the model is for. */\n .v-file-input[data-display='text'] .v-input-control {\n text-overflow: ellipsis;\n }\n\n /* While a file is being dragged over it, the field is highlighted by redefining the\n very variable VInput uses for its own border colour — so the two can never disagree.\n\n TRAP — the selector weighs (0,4,0), one step above VInput's states, and it has to. The\n field inside is ALWAYS read-only, and VInput's read-only background weighs (0,3,0): at\n that weight the winner would be whichever of the two sheets a bundler put last. */\n .v-file-input[data-dragging] .v-input .v-input-field {\n --field-border-color: var(--vectis-color-accent);\n\n background: var(--vectis-color-accent-surface);\n }\n\n .v-file-input[data-disabled] .v-file-input-hint,\n .v-file-input[data-disabled] .v-file-input-counter {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VFileInput.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VFileInput/VFileInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * Compose a read-only VInput with a native file input. useFileField bridges File[] selection to\n * the browser dialog and form submission.\n */\nimport { computed, inject, ref, useId } from 'vue'\n\nimport VChip from '../VChip/VChip.vue'\nimport type { ChipSize } from '../VChip/VChip.vue'\nimport { attach_file as attachFileIcon } from '../VIcon/icons/attach_file'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport { inputGroupKey } from '../VInput/context'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { canClear } from '../../composables/useClearable'\nimport { useControlShape } from '../../composables/useControlShape'\nimport { useFileDrop } from '../../composables/useFileDrop'\nimport { useFileField } from '../../composables/useFileField'\nimport { iconStartListener } from '../../composables/useIconClickHandlers'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { chipScaleFor } from '../../utils/chip'\nimport { isDev } from '../../utils/env'\nimport { fileKey, formatBytes, type FileRejection } from '../../utils/file'\nimport { joinIds } from '../../utils/ids'\nimport { truncateMiddle } from './truncate'\n\n/** How the chosen files are shown inside the field when several are allowed. */\nexport type FileInputDisplay = 'chip' | 'text'\n\n/** What the `#chip` slot receives. */\nexport interface FileInputChipSlotProps {\n /** The file the chip stands for. */\n file: File\n /** Its position in the selection. */\n index: number\n /** Its name, already shortened in the middle so the extension survives. */\n label: string\n /** Takes the file out of the selection, the only way to. */\n remove: () => void\n /** The chip size worked out to sit inside the field, one step below it. */\n size: ChipSize\n /** Whether the chip takes its compact height, to catch up below the smallest step. */\n compact: boolean\n}\n\n/** What the `#counter` slot receives. */\nexport interface FileInputCounterSlotProps {\n /** How many files are selected. */\n count: number\n /** Their total size, in bytes. */\n bytes: number\n /** The sentence the counter shows by default, already translated. */\n text: string\n}\n\n/** The height of the field: 32, 40 or 48 pixels. */\nexport type FileInputSize = 'sm' | 'md' | 'lg'\n\ninterface FileInputProps {\n /** Allows several files to be chosen. With one only, every extra file is turned away. */\n multiple?: boolean\n /** Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`). */\n accept?: string\n /**\n * How the chosen files are shown: their names joined by commas, or one dismissible\n * chip each. It only means something when several files are allowed; a single name is\n * always text.\n */\n display?: FileInputDisplay\n /** The largest ONE file may be, in bytes. */\n maxSize?: number\n /** The largest the whole selection may be, in bytes. */\n maxTotalSize?: number\n /** How many files may be chosen at most. */\n maxFiles?: number\n /** Shows how much has been chosen under the field: \"3 files (1.2 MB)\". */\n counter?: boolean\n /** The icon at the end of the field, which opens the file dialog. */\n pickerIcon?: IconSource\n /** Refuses files dropped onto the component: only the dialog then adds any. */\n noDrop?: boolean\n /** The height of the field: 32, 40 or 48 pixels. */\n size?: FileInputSize\n /** Takes 4px off the height, leaving the padding, the text and the icons as they are. */\n compact?: boolean\n /** Makes the field unusable, greyed out through the colour tokens. */\n disabled?: boolean\n /**\n * Shows what was chosen without allowing it to change: no dialog, no drop, no removal.\n */\n readonly?: boolean\n /**\n * Marks the field as invalid, for a rule of your own, since nothing here is checked\n * by the browser.\n */\n invalid?: boolean\n /** The label above the field, tied to it so that clicking it focuses the field. */\n label?: string\n /**\n * A line of help under the field, to the left of the counter. It is tied to the field\n * for assistive technology.\n */\n hint?: string\n /**\n * What the field says while nothing is chosen. It falls back to the design system\n * dictionary.\n */\n placeholder?: string\n /**\n * An icon inside the field, at the start. Decorative by default, it becomes a real button as\n * soon as a `@click:icon-start` listener is attached, in which case it needs\n * `iconStartLabel`.\n */\n iconStart?: IconSource\n /** What the start icon does, in words, once it is clickable. */\n iconStartLabel?: string\n /**\n * What the button at the end of the field does, in words. It names the button\n * `pickerIcon` renders, and falls back to the design system dictionary.\n */\n pickerIconLabel?: string\n /**\n * Shows a spinner at the end of the field, in place of the attach icon, while an upload is\n * under way, typically.\n */\n loading?: boolean\n /**\n * What screen readers announce while the spinner turns. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /** Offers a cross that empties the selection. */\n clearable?: boolean\n /** What that cross does, in words. It falls back to the design system dictionary. */\n clearLabel?: string\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<FileInputProps>(), {\n multiple: false,\n accept: undefined,\n display: 'text',\n maxSize: undefined,\n maxTotalSize: undefined,\n maxFiles: undefined,\n counter: false,\n pickerIcon: () => attachFileIcon,\n noDrop: false,\n size: 'md',\n compact: false,\n disabled: false,\n readonly: false,\n invalid: false,\n label: undefined,\n hint: undefined,\n placeholder: undefined,\n iconStart: undefined,\n iconStartLabel: undefined,\n pickerIconLabel: undefined,\n loading: false,\n loadingText: undefined,\n clearable: false,\n clearLabel: undefined,\n})\n\nconst emit = defineEmits<{\n /** The reader changed the selection: added something, removed something, cleared it. */\n change: [files: File[]]\n /**\n * A file was turned away and never joined the selection. It is emitted once PER file,\n * so a batch drop can be reported precisely.\n */\n reject: [rejection: FileRejection]\n /**\n * ONE file was taken out through its chip, with the file and the position it held.\n * `change` follows it with the whole list, as it does after every other change.\n */\n remove: [file: File, index: number]\n /** The clear cross emptied the selection. */\n clear: []\n /** The start icon was clicked. Attaching this listener is what makes it a button. */\n 'click:icon-start': [event: MouseEvent]\n}>()\n\ndefineSlots<{\n /**\n * Content at the start of the field, rendered after the chips standing for the chosen\n * files rather than in their place.\n */\n start?(): unknown\n /**\n * Controls of your own inside the field, placed before the ones the field owns: the clear\n * cross and the icon that opens the file dialog.\n */\n 'value-end'?(): unknown\n /** Replaces the chip standing for one file. */\n chip?(props: FileInputChipSlotProps): unknown\n /**\n * Replaces the counter under the field. `text` is the sentence already built and\n * translated; the count and the total size in `bytes` are there for a wording of your own.\n */\n counter?(props: FileInputCounterSlotProps): unknown\n}>()\n\n// `get` reads an explicit `null` as the empty list: the default applies only to a model\n// that was never given, and `files = ref(null)` is a common start in untyped code.\n/** Always a LIST of files, whether or not several are allowed, and never a file on its own. */\nconst model = defineModel<File[]>({ default: () => [], get: (files) => files ?? [] })\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\n\n// Declared as this component's own event, `click:icon-start` is out of `$attrs`, so the\n// listener is relayed to the field by hand; and only when the consumer wrote one.\nconst iconStartClick = iconStartListener((event) => emit('click:icon-start', event))\n\nconst m = useMessages()\nconst locale = useLocale()\n\n// A VInputGroup joins several controls into one object, so the shape of this field is the\n// row's decision rather than its own (VInput/context.ts). The chips read the resolved pair\n// too, or they would come out one step off the field they sit in.\nconst group = inject(inputGroupKey, null)\nconst {\n size: resolvedSize,\n compact: resolvedCompact,\n disabled: resolvedDisabled,\n} = useControlShape(props, group)\n\n/*\n * Here, the bucket meant for \"the control the user deals with\" goes to the visible field: that\n * is what they see, focus and click, so a consumer's own `<label for>` has to point at it. The\n * description attribute is pulled out of that bucket, because this component re-assembles it\n * further down.\n */\nconst {\n fileEl,\n enabled,\n nativeInputAttrs,\n controlAttrs,\n acceptFiles,\n openPicker,\n removeAt,\n clear,\n} = useFileField({\n model,\n props,\n emit,\n forwardedAttrs,\n disabled: resolvedDisabled,\n excludeFromControl: ['aria-describedby'],\n onClear: () => emit('clear'),\n warnings: isDev\n ? () => [\n props.display === 'chip' &&\n !props.multiple &&\n 'display=\"chip\" ignored without `multiple`: a single file shows as text.',\n ]\n : undefined,\n})\n\n/** What reaches the visible field: the consumer's own attributes, plus that listener. */\nconst fieldAttrs = computed(() => ({ ...controlAttrs.value, ...iconStartClick }))\n\nconst inputRef = ref<InstanceType<typeof VInput> | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst resolvedDisplay = computed<FileInputDisplay>(() => (props.multiple ? props.display : 'text'))\n\nconst displayText = computed(() =>\n resolvedDisplay.value === 'chip' ? '' : model.value.map((file) => file.name).join(', '),\n)\n\nconst placeholderText = computed(() =>\n model.value.length > 0 ? undefined : (props.placeholder ?? m.value.fileInput.placeholder),\n)\n\n/*\n * The visible field is read-only, and a read-only field hides its cross by default; rightly so,\n * since its text cannot be edited. Here the value comes from the file dialog rather than from\n * typing, so there is something to clear all the same.\n */\nconst clearVisible = computed(() => canClear(props, resolvedDisabled.value, model.value.length > 0))\n\n// @a11y @devwarn\n/*\n * Its label stays defined always, even when no icon is rendered. The helper that detects a\n * click handler on an icon warns AT setup if one is attached without a label, and it has no way\n * of knowing whether an icon exists.\n */\nconst endIcon = computed<IconSource | undefined>(() =>\n props.readonly ? undefined : props.pickerIcon,\n)\nconst endIconLabel = computed(() => props.pickerIconLabel ?? m.value.fileInput.openPicker)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? m.value.fileInput.clear)\n\nconst totalSize = computed(() => model.value.reduce((sum, file) => sum + file.size, 0))\n\n/*\n * The counter is assembled from three sources: the WORD comes from the dictionary, since it is\n * language; the SIZE is formatted by the platform, which knows the local conventions; and the\n * parentheses come from neither, being punctuation every language shares; the same boundary\n * VBadge's \"99+\" falls on. With nothing chosen it states the count alone: \"0 files ()\" would be\n * noise.\n */\nconst counterText = computed(() => {\n const word = m.value.fileInput.files(model.value.length)\n return model.value.length === 0 ? word : `${word} (${formatBytes(totalSize.value, locale.value)})`\n})\n\nconst hintId = useId()\nconst counterId = useId()\n\n// @a11y\n/*\n * What describes the field for a screen reader is a LIST of references, assembled here rather\n * than by VInput: the hint belongs to OUR row under the field, VInput having no such row; its\n * own counter sits inside the field. The counter is deliberately part of that list, unlike in\n * VTextarea.\n */\nconst describedBy = computed(() =>\n joinIds(\n attrs['aria-describedby'] as string | undefined,\n !!props.hint && hintId,\n props.counter && counterId,\n ),\n)\n\n// The size, the density and the HEIGHT of the chips sitting inside the field, worked out\n// once in `utils/chip.ts` and shared with VCombobox. The height is set inline rather than\n// restated as a table of CSS rules: it belongs to the chips' own subtree, out of the\n// field's reach, and the field has to force its input to that same height or it grows\n// when focused.\nconst chipScale = computed(() => chipScaleFor(resolvedSize.value, resolvedCompact.value))\n\n/**\n * What each chip shows for its file, worked out once per change of the selection rather than on\n * every render, which reads it three times per chip.\n */\n// Computed lazily, so a text display, which never reads it, never pays for it.\nconst chipLabels = computed(() => model.value.map((file) => truncateMiddle(file.name)))\n\nfunction onControlClick(event: MouseEvent) {\n // Without this guard the attach icon would open the dialog TWICE, and the clear cross would\n // reopen it immediately after emptying the selection.\n if ((event.target as HTMLElement).closest('button')) return\n openPicker()\n}\n\nfunction onFieldKeydown(event: KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return\n // Cancelling the default on Enter also stops the form from being submitted: a\n // read-only text field is still a field, and Enter in one submits the form around it.\n event.preventDefault()\n openPicker()\n}\n\nfunction removeFile(index: number) {\n // The removal button disappears along with its chip, so the focus would fall back to\n // the page body. Unlike the date and time pickers, nothing here opens on focus, so\n // this needs no guard against re-entering.\n if (removeAt(index)) inputRef.value?.focus()\n}\n\n/*\n * Files may be dropped on the component itself; there is no separate drop area here; that is\n * what VFilePicker is for.\n */\nconst { dragging, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop(\n () => !props.noDrop && enabled.value,\n acceptFiles,\n)\n\ndefineExpose({\n /** Moves the focus to the visible field. */\n focus: (options?: FocusOptions) => inputRef.value?.focus(options),\n /**\n * Opens the file dialog. It only works when called from something the reader did (a\n * click, a key press): browsers refuse to open a file dialog by themselves.\n */\n open: openPicker,\n /**\n * The component's own box, for what neither of the two above covers. It is the wrapper\n * and not the hidden file input: that one is the FORM's element, this is the reader's.\n */\n el: rootEl,\n})\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n class=\"v-file-input\"\n :class=\"rootClass\"\n :style=\"[{ '--chip-height': chipScale.height }, rootStyle]\"\n :data-size=\"resolvedSize\"\n :data-compact=\"resolvedCompact ? '' : undefined\"\n :data-display=\"resolvedDisplay\"\n :data-disabled=\"resolvedDisabled ? '' : undefined\"\n :data-readonly=\"readonly ? '' : undefined\"\n :data-dragging=\"dragging ? '' : undefined\"\n @dragenter=\"onDragEnter\"\n @dragover=\"onDragOver\"\n @dragleave=\"onDragLeave\"\n @drop=\"onDrop\"\n >\n <input v-bind=\"nativeInputAttrs\" ref=\"fileEl\" class=\"v-file-input-native v-hidden-input\" />\n\n <div class=\"v-file-input-control\" @click=\"onControlClick\">\n <!-- Shown as chips, the field takes VInput's two arrangements for chips: its end\n controls lifted out of the flow, and the chips wrapping onto several rows. -->\n <VInput\n v-bind=\"fieldAttrs\"\n ref=\"inputRef\"\n :class=\"{ 'v-input-end-pinned v-input-chips': resolvedDisplay === 'chip' }\"\n :model-value=\"displayText\"\n no-typing\n :readonly=\"readonly\"\n :label=\"label\"\n :placeholder=\"placeholderText\"\n :size=\"resolvedSize\"\n :compact=\"resolvedCompact\"\n :disabled=\"resolvedDisabled\"\n :invalid=\"invalid\"\n :clearable=\"clearable\"\n :clear-visible=\"clearVisible\"\n :clear-label=\"resolvedClearLabel\"\n :icon-start=\"iconStart\"\n :icon-start-label=\"iconStartLabel\"\n :loading=\"loading\"\n :loading-text=\"loadingText\"\n :icon-end=\"endIcon\"\n :icon-end-label=\"endIconLabel\"\n :aria-describedby=\"describedBy\"\n @click:icon-end=\"openPicker\"\n @clear=\"clear\"\n @keydown=\"onFieldKeydown\"\n >\n <template v-if=\"resolvedDisplay === 'chip' || $slots.start\" #start>\n <template\n v-for=\"(file, index) in resolvedDisplay === 'chip' ? model : []\"\n :key=\"fileKey(file)\"\n >\n <slot\n name=\"chip\"\n :file=\"file\"\n :index=\"index\"\n :label=\"chipLabels[index]!\"\n :remove=\"() => removeFile(index)\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n >\n <VChip\n tone=\"accent\"\n :size=\"chipScale.size\"\n :compact=\"chipScale.compact\"\n :dismissible=\"enabled\"\n :dismiss-label=\"m.common.remove(file.name)\"\n :disabled=\"resolvedDisabled\"\n :title=\"chipLabels[index] === file.name ? undefined : file.name\"\n @dismiss=\"removeFile(index)\"\n >{{ chipLabels[index] }}</VChip\n >\n </slot>\n </template>\n <slot name=\"start\" />\n </template>\n\n <template v-if=\"$slots['value-end']\" #value-end><slot name=\"value-end\" /></template>\n </VInput>\n </div>\n\n <div v-if=\"hint || counter\" class=\"v-file-input-meta v-field-meta\">\n <VTypography\n v-if=\"hint\"\n :id=\"hintId\"\n variant=\"caption\"\n tone=\"muted\"\n class=\"v-file-input-hint\"\n >\n {{ hint }}\n </VTypography>\n <span v-if=\"counter\" :id=\"counterId\" class=\"v-file-input-counter v-field-counter\">\n <slot name=\"counter\" :count=\"model.length\" :bytes=\"totalSize\" :text=\"counterText\">\n {{ counterText }}\n </slot>\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-file-input {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n width: 100%;\n font-family: var(--vectis-text-family);\n }\n\n /*\n * The real file input is a source of files and not a control anyone deals with: it wears\n * `.v-hidden-input`, and taking it out of the tab order and hiding it from screen readers, in\n * `useFileField`, is what leaves the visible field as the single stop and the single\n * announcement.\n */\n .v-file-input:not([data-disabled]):not([data-readonly]) .v-file-input-control,\n .v-file-input:not([data-disabled]):not([data-readonly])[data-display='text'] .v-input-control {\n cursor: pointer;\n }\n\n /* An ellipsis DOES work on an input, and a read-only one never scrolls by itself, so a\n long list of names is simply cut short. No tooltip is added for the rest: it would be\n announced as a description repeating the value the field already shows, and the\n complete list is what the model is for. */\n .v-file-input[data-display='text'] .v-input-control {\n text-overflow: ellipsis;\n }\n\n /*\n * The selector weighs (0,4,0), one step above VInput's states, and it has to. The field\n * inside is always read-only, and VInput's read-only background weighs (0,3,0): at that\n * weight the winner would be whichever of the two sheets a bundler put last.\n */\n .v-file-input[data-dragging] .v-input .v-input-field {\n --field-border-color: var(--vectis-color-accent);\n\n background: var(--vectis-color-accent-surface);\n }\n\n .v-file-input[data-disabled] .v-file-input-hint,\n .v-file-input[data-disabled] .v-file-input-counter {\n color: var(--vectis-color-text-subtle);\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* extension, which is often the only thing distinguishing two entries
|
|
5
|
-
* (`invoice.pdf` and `invoice.png`), and the tail is also where the version or
|
|
6
|
-
* date suffixes live.
|
|
7
|
-
*
|
|
8
|
-
* Pure, no Vue: the `VHotkeys/platform.ts` precedent: a single consumer, so it
|
|
9
|
-
* stays in the component's folder. Deliberately NOT promoted alongside the file
|
|
10
|
-
* helpers of `utils/file.ts`: cutting a string is a lexical operation, not
|
|
11
|
-
* something the FILE domain decides: `utils/text.ts` is where it would go the
|
|
12
|
-
* day a second consumer appears.
|
|
2
|
+
* Preserve both filename ends when truncating so extensions and version suffixes remain
|
|
3
|
+
* identifiable.
|
|
13
4
|
*/
|
|
14
5
|
/** Ceiling of a chip's label, ellipsis included. Chips wrap, so the point is not
|
|
15
6
|
the field's width but keeping a row readable at a glance. */
|
|
16
7
|
export declare const CHIP_NAME_MAX = 20;
|
|
17
|
-
/**
|
|
18
|
-
* `truncateMiddle('annual_report_2026_final.pdf')` → `annual_rep…_final.pdf`.
|
|
19
|
-
*
|
|
20
|
-
* The result is EXACTLY `max` characters long: the ellipsis takes one of them,
|
|
21
|
-
* and the odd one goes to the head: the start of a name is what makes it
|
|
22
|
-
* recognizable, the end is what disambiguates it.
|
|
23
|
-
*
|
|
24
|
-
* Counted in CODE POINTS (`[...text]`) and not in UTF-16 units: an emoji in a
|
|
25
|
-
* name would otherwise be cut between its two halves and render as a
|
|
26
|
-
* replacement character. Combining marks stay out of scope: a grapheme
|
|
27
|
-
* segmenter for a chip label would be a lot of machinery for a rounding error.
|
|
28
|
-
*/
|
|
8
|
+
/** `truncateMiddle('annual_report_2026_final.pdf')` → `annual_rep…_final.pdf`. */
|
|
29
9
|
export declare function truncateMiddle(text: string, max?: number): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"truncate.js","names":[],"sources":["../../../src/components/VFileInput/truncate.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"truncate.js","names":[],"sources":["../../../src/components/VFileInput/truncate.ts"],"sourcesContent":["/**\n * Preserve both filename ends when truncating so extensions and version suffixes remain\n * identifiable.\n */\n\n/** Ceiling of a chip's label, ellipsis included. Chips wrap, so the point is not\n the field's width but keeping a row readable at a glance. */\nexport const CHIP_NAME_MAX = 20\n\n/** `truncateMiddle('annual_report_2026_final.pdf')` → `annual_rep…_final.pdf`. */\nexport function truncateMiddle(text: string, max: number = CHIP_NAME_MAX): string {\n const chars = [...text]\n if (chars.length <= max) return text\n\n const budget = Math.max(0, max - 1)\n const head = Math.ceil(budget / 2)\n const tail = budget - head\n\n return `${chars.slice(0, head).join('')}…${chars.slice(chars.length - tail).join('')}`\n}\n"],"mappings":"AAUA,SAAgB,EAAe,GAAc,IAAA,IAAqC;CAChF,IAAM,IAAQ,CAAC,GAAG,CAAI;CACtB,IAAI,EAAM,UAAU,GAAK,OAAO;CAEhC,IAAM,IAAS,KAAK,IAAI,GAAG,IAAM,CAAC,GAC5B,IAAO,KAAK,KAAK,IAAS,CAAC,GAC3B,IAAO,IAAS;CAEtB,OAAO,GAAG,EAAM,MAAM,GAAG,CAAI,CAAC,CAAC,KAAK,EAAE,EAAE,GAAG,EAAM,MAAM,EAAM,SAAS,CAAI,CAAC,CAAC,KAAK,EAAE;AACrF"}
|
|
@@ -32,14 +32,7 @@ export interface FilePickerRow {
|
|
|
32
32
|
index: number;
|
|
33
33
|
/** What kind of file it is, as worked out from its type or its extension. */
|
|
34
34
|
kind: FilePickerKind;
|
|
35
|
-
/**
|
|
36
|
-
* The address of its image thumbnail, when there is one.
|
|
37
|
-
*
|
|
38
|
-
* It is absent on the server AND on the browser's first render, since those addresses
|
|
39
|
-
* only exist in a browser and creating them during the first render would make the two
|
|
40
|
-
* markups differ. It is absent as well for anything the browser fails to decode. Fall
|
|
41
|
-
* back to the icon in that case.
|
|
42
|
-
*/
|
|
35
|
+
/** The address of its image thumbnail, when there is one. Fall back to the icon in that case. */
|
|
43
36
|
thumbnail: string | undefined;
|
|
44
37
|
/** The icon standing for that kind of file, a consumer's override included. */
|
|
45
38
|
icon: IconSource;
|
|
@@ -49,24 +42,13 @@ export interface FilePickerRow {
|
|
|
49
42
|
remove: () => void;
|
|
50
43
|
}
|
|
51
44
|
interface FilePickerProps {
|
|
52
|
-
/**
|
|
53
|
-
* What the reader is being asked to drop, in one line. It is REQUIRED: a drop zone
|
|
54
|
-
* with no instruction is just a rectangle.
|
|
55
|
-
*
|
|
56
|
-
* Note that this prop shadows the HTML attribute of the same name on the component
|
|
57
|
-
* itself, an accepted trade-off: a tooltip on a drop zone would be redundant anyway.
|
|
58
|
-
*/
|
|
45
|
+
/** What the reader is being asked to drop, in one line. */
|
|
59
46
|
title: string;
|
|
60
47
|
/** A second line under it, for the constraints in plain words: kinds, sizes, how many. */
|
|
61
48
|
subtitle?: string;
|
|
62
49
|
/** The large icon at the top of the zone. */
|
|
63
50
|
icon?: IconSource;
|
|
64
|
-
/**
|
|
65
|
-
* Hides the separator and the browse button under the instruction. That changes the
|
|
66
|
-
* zone's nature: it then becomes the control ITSELF, a real button, so Enter, Space
|
|
67
|
-
* and the focus come from the platform rather than from a container that merely
|
|
68
|
-
* reacts to clicks.
|
|
69
|
-
*/
|
|
51
|
+
/** Hides the separator and the browse button under the instruction. */
|
|
70
52
|
hideBrowse?: boolean;
|
|
71
53
|
/**
|
|
72
54
|
* The wording drawn on the browse button, which is also its accessible name. It falls
|
|
@@ -74,12 +56,8 @@ interface FilePickerProps {
|
|
|
74
56
|
*/
|
|
75
57
|
browseText?: string;
|
|
76
58
|
/**
|
|
77
|
-
* Where the files taken are listed: under the zone, or beside it.
|
|
78
|
-
*
|
|
79
|
-
* than the width of the window.
|
|
80
|
-
*
|
|
81
|
-
* By default nothing is listed at all: the value is there for a parent that wants to
|
|
82
|
-
* present it its own way.
|
|
59
|
+
* Where the files taken are listed: under the zone, or beside it. By default nothing is
|
|
60
|
+
* listed at all: the value is there for a parent that wants to present it its own way.
|
|
83
61
|
*/
|
|
84
62
|
preview?: FilePickerPreview;
|
|
85
63
|
/**
|
|
@@ -97,13 +75,7 @@ interface FilePickerProps {
|
|
|
97
75
|
removeIcon?: IconSource;
|
|
98
76
|
/** Allows several files to be taken. With one only, every extra file is turned away. */
|
|
99
77
|
multiple?: boolean;
|
|
100
|
-
/**
|
|
101
|
-
* Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`).
|
|
102
|
-
*
|
|
103
|
-
* It is applied TWICE, and it has to be: as an attribute, which is what filters the
|
|
104
|
-
* system's file dialog, and again in code, which is the only thing that can filter a
|
|
105
|
-
* DROPPED file, the attribute having no say over a drop.
|
|
106
|
-
*/
|
|
78
|
+
/** Which kinds of file are accepted, in the browser's own syntax (`image/*,.pdf`). */
|
|
107
79
|
accept?: string;
|
|
108
80
|
/** The largest ONE file may be, in bytes. */
|
|
109
81
|
maxSize?: number;
|
|
@@ -119,16 +91,13 @@ interface FilePickerProps {
|
|
|
119
91
|
*/
|
|
120
92
|
readonly?: boolean;
|
|
121
93
|
/**
|
|
122
|
-
* Marks the zone as invalid, which colours its outline and is announced on the control
|
|
123
|
-
*
|
|
124
|
-
* the real input being hidden.
|
|
94
|
+
* Marks the zone as invalid, which colours its outline and is announced on the control the
|
|
95
|
+
* reader reaches.
|
|
125
96
|
*/
|
|
126
97
|
invalid?: boolean;
|
|
127
98
|
/**
|
|
128
99
|
* Shows a spinner in place of the zone icon, typically while an upload is under way.
|
|
129
|
-
*
|
|
130
|
-
* dropped and the dialog still opens. `disabled` and `readonly` are the props that cut
|
|
131
|
-
* those off.
|
|
100
|
+
* `disabled` and `readonly` are the props that cut those off.
|
|
132
101
|
*/
|
|
133
102
|
loading?: boolean;
|
|
134
103
|
/**
|
|
@@ -139,26 +108,17 @@ interface FilePickerProps {
|
|
|
139
108
|
}
|
|
140
109
|
type __VLS_Props = FilePickerProps;
|
|
141
110
|
type __VLS_Slots = {
|
|
142
|
-
/**
|
|
143
|
-
* The large icon, for an illustration the icon props cannot express.
|
|
144
|
-
*
|
|
145
|
-
* It must stay non-interactive, and so must the two below: with the browse button
|
|
146
|
-
* hidden, the zone IS a button, and nothing interactive may sit inside one.
|
|
147
|
-
*/
|
|
111
|
+
/** The large icon, for an illustration the icon props cannot express. */
|
|
148
112
|
icon?(): unknown;
|
|
149
113
|
/** The instruction. Text and inline elements only, for the same reason. */
|
|
150
114
|
title?(): unknown;
|
|
151
115
|
/** The second line. Same contract as the instruction. */
|
|
152
116
|
subtitle?(): unknown;
|
|
153
|
-
/**
|
|
154
|
-
* The browse button. Call the `open` it receives: without it a button of your own
|
|
155
|
-
* could no longer open the file dialog at all.
|
|
156
|
-
*/
|
|
117
|
+
/** The browse button. */
|
|
157
118
|
browse?(props: FilePickerBrowseSlotProps): unknown;
|
|
158
119
|
/**
|
|
159
|
-
* A
|
|
160
|
-
*
|
|
161
|
-
* again.
|
|
120
|
+
* A whole row of the list, the way out for a row showing its own upload progress. It receives
|
|
121
|
+
* everything the standard row was given, so nothing has to be worked out again.
|
|
162
122
|
*/
|
|
163
123
|
item?(props: FilePickerRow): unknown;
|
|
164
124
|
/**
|
|
@@ -166,21 +126,11 @@ type __VLS_Slots = {
|
|
|
166
126
|
* video's poster frame, or a format the browser cannot decode.
|
|
167
127
|
*/
|
|
168
128
|
thumbnail?(props: FilePickerRow): unknown;
|
|
169
|
-
/**
|
|
170
|
-
* The control that removes a row. Two of the values it receives are not optional in
|
|
171
|
-
* practice: `remove` is the only thing that can take the file out, and `removeLabel` is
|
|
172
|
-
* the ready-made accessible name, the file's own included, without which the button
|
|
173
|
-
* would be announced as nothing at all.
|
|
174
|
-
*/
|
|
129
|
+
/** The control that removes a row. */
|
|
175
130
|
remove?(props: FilePickerRemoveSlotProps): unknown;
|
|
176
131
|
};
|
|
177
132
|
type __VLS_ModelProps = {
|
|
178
|
-
/**
|
|
179
|
-
* Always a LIST of files, whether or not several are allowed, and never a file on its own.
|
|
180
|
-
* The shape of the value does not depend on a prop, so a consumer never has to narrow a
|
|
181
|
-
* union TypeScript has no way of discriminating. With a single file it is simply a list
|
|
182
|
-
* of at most one.
|
|
183
|
-
*/
|
|
133
|
+
/** Always a LIST of files, whether or not several are allowed, and never a file on its own. */
|
|
184
134
|
modelValue?: File[];
|
|
185
135
|
};
|
|
186
136
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|