@feugene/granularity 0.31.0 → 0.32.0
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/README.md +0 -1
- package/dist/chunks/adapter-CAOxmn2G.js.map +1 -1
- package/dist/chunks/{config-BHcS5Cwm.js → config-87fszqnD.js} +1 -1
- package/dist/chunks/{config-BHcS5Cwm.js.map → config-87fszqnD.js.map} +1 -1
- package/dist/chunks/config-A-bBiotF.js.map +1 -1
- package/dist/chunks/config-B3J0alyY.js.map +1 -1
- package/dist/chunks/config-BhfNgyWL.js.map +1 -1
- package/dist/chunks/config-Bjn30smH.js.map +1 -1
- package/dist/chunks/config-BqT6PMzO.js.map +1 -1
- package/dist/chunks/config-C0Ea9ePP.js.map +1 -1
- package/dist/chunks/config-C0Eu8jpG.js.map +1 -1
- package/dist/chunks/config-CDat1wXf.js.map +1 -1
- package/dist/chunks/config-CQLNjc9X2.js.map +1 -1
- package/dist/chunks/config-CbEbLyDE.js.map +1 -1
- package/dist/chunks/{config-jyr6AIZt2.js → config-CuQmUkMw2.js} +2 -2
- package/dist/chunks/{config-jyr6AIZt2.js.map → config-CuQmUkMw2.js.map} +1 -1
- package/dist/chunks/config-DeyT9dJl.js.map +1 -1
- package/dist/chunks/config-DnF472Ox.js.map +1 -1
- package/dist/chunks/config-DwABJHEo.js.map +1 -1
- package/dist/chunks/{config-tiUtEn4Q.js → config-E1cTyRVV.js} +1 -1
- package/dist/chunks/{config-tiUtEn4Q.js.map → config-E1cTyRVV.js.map} +1 -1
- package/dist/chunks/config-MmPym-Z72.js.map +1 -1
- package/dist/chunks/config-T3-DMXYm.js.map +1 -1
- package/dist/chunks/config-Yy67uWHY.js.map +1 -1
- package/dist/chunks/config-_jbgsFpw.js.map +1 -1
- package/dist/chunks/config-g8-JGZ_Y.js.map +1 -1
- package/dist/chunks/config-mlheIMtV.js.map +1 -1
- package/dist/chunks/config-rLkc9Zn2.js.map +1 -1
- package/dist/chunks/config-uzUuosv7.js.map +1 -1
- package/dist/chunks/context-CTbuwcYs.js.map +1 -1
- package/dist/chunks/deltaTone-B5OoS1vo.js.map +1 -1
- package/dist/chunks/es-Do2CNrC2.js.map +1 -1
- package/dist/chunks/fileValidation-D5TFBhbv.js.map +1 -1
- package/dist/chunks/focusables-BkzK42SQ.js.map +1 -1
- package/dist/chunks/{generated-DJ2dJ__C.js → generated-CSyPsWtM.js} +1 -1
- package/dist/chunks/{generated-DJ2dJ__C.js.map → generated-CSyPsWtM.js.map} +1 -1
- package/dist/chunks/grBadgeStyles-D8Mz73B5.js.map +1 -1
- package/dist/chunks/{grButtonStyles-B_-YRE__.js → grButtonStyles-B3wRWW_i.js} +4 -4
- package/dist/chunks/grButtonStyles-B3wRWW_i.js.map +1 -0
- package/dist/chunks/grLinkStyles-FME_qMpi.js.map +1 -1
- package/dist/chunks/grLoadingStyles-CnQaf5ry.js.map +1 -1
- package/dist/chunks/{grRadioStyles-D4gnU65n.js → grRadioStyles-DGL2QLy6.js} +1 -1
- package/dist/chunks/{grRadioStyles-D4gnU65n.js.map → grRadioStyles-DGL2QLy6.js.map} +1 -1
- package/dist/chunks/grSelectStyles-Cdtd10eY.js.map +1 -1
- package/dist/chunks/{grSliderStyles-B5uNxNJq.js → grSliderStyles-kaz6VXZ3.js} +9 -1
- package/dist/chunks/grSliderStyles-kaz6VXZ3.js.map +1 -0
- package/dist/chunks/{granular-provider-yJNX1g1G.js → granular-provider-Da2CJO6j.js} +3 -3
- package/dist/chunks/{granular-provider-yJNX1g1G.js.map → granular-provider-Da2CJO6j.js.map} +1 -1
- package/dist/chunks/granularityI18n-BC5zsj_d.js.map +1 -1
- package/dist/chunks/icon-Dag2tIMs.js.map +1 -1
- package/dist/chunks/inert-DIHCCn_1.js.map +1 -1
- package/dist/chunks/keyboard-DE2yhz_b.js.map +1 -1
- package/dist/chunks/numericStep-Bq2lj4ew.js.map +1 -1
- package/dist/chunks/optionFilter-DYw9XPPx.js.map +1 -1
- package/dist/chunks/{overlayStack-C8cfsqXa.js → overlayStack-0CZS99Da.js} +40 -6
- package/dist/chunks/overlayStack-0CZS99Da.js.map +1 -0
- package/dist/chunks/portalRoot-CVhCgdG6.js.map +1 -1
- package/dist/chunks/renamedProp-BwcN-SJg.js.map +1 -1
- package/dist/chunks/{safelist-BOq5Li7p.js → safelist-DxkMj5IW.js} +1 -1
- package/dist/chunks/{safelist-BOq5Li7p.js.map → safelist-DxkMj5IW.js.map} +1 -1
- package/dist/chunks/slotNodes-DxNXpC7v.js.map +1 -1
- package/dist/chunks/useControlAddons-asUr10Pf.js.map +1 -1
- package/dist/chunks/useControlledOpen-DV3SqLTP.js.map +1 -1
- package/dist/chunks/useDragSort-0l47h4yb.js.map +1 -1
- package/dist/chunks/useFilePreviews-DnwgdyBs.js.map +1 -1
- package/dist/chunks/useFocusWithin-BwHE0rzm.js.map +1 -1
- package/dist/chunks/{useModalOverlay-Cv5hF63W.js → useModalOverlay-D__CqLGn.js} +3 -0
- package/dist/chunks/useModalOverlay-D__CqLGn.js.map +1 -0
- package/dist/chunks/usePortalTarget-C0p-mhaM.js.map +1 -1
- package/dist/chunks/validation-5HhpanPP.js.map +1 -1
- package/dist/components/GrAlert/chunks/GrAlert-FAVf-ksH.js.map +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-X6RlmiJQ.js → GrAutocomplete-rYEY15UI.js} +1 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-rYEY15UI.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrAvatar/chunks/GrAvatar-CNmLmgcr.js.map +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-BsoocpF-.js → GrBadge-D2MfThPz.js} +1 -1
- package/dist/components/GrBadge/chunks/GrBadge-D2MfThPz.js.map +1 -0
- package/dist/components/GrBadge/index.js +1 -1
- package/dist/components/GrBadge/styles.css +1 -1
- package/dist/components/GrBadgeWrap/chunks/GrBadgeWrap-B-A3PRkE.js.map +1 -1
- package/dist/components/GrBottomNav/chunks/GrBottomNav-wtFatLjB.js.map +1 -1
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DRDNan_n.js.map +1 -1
- package/dist/components/GrButton/chunks/{GrButton-Bw2AH4uX.js → GrButton-7GP0zObj.js} +1 -1
- package/dist/components/GrButton/chunks/GrButton-7GP0zObj.js.map +1 -0
- package/dist/components/GrButton/index.js +3 -3
- package/dist/components/GrButtonGroup/chunks/GrButtonGroup-CKyiAV8K.js.map +1 -1
- package/dist/components/GrCard/chunks/GrCard-BttXz1pQ.js.map +1 -1
- package/dist/components/GrCheckbox/chunks/GrCheckbox-CEcHIsXM.js.map +1 -1
- package/dist/components/GrCheckboxGroup/chunks/GrCheckboxGroup-BOwrVcFr.js.map +1 -1
- package/dist/components/GrChip/chunks/GrChip-C0vU69tf.js.map +1 -1
- package/dist/components/GrChipGroup/chunks/GrChipGroup-C78Vcwos.js.map +1 -1
- package/dist/components/GrCodeBlock/chunks/{GrCodeBlock-Dkvkr91t.js → GrCodeBlock-Dp0eEBTZ.js} +1 -1
- package/dist/components/GrCodeBlock/chunks/GrCodeBlock-Dp0eEBTZ.js.map +1 -0
- package/dist/components/GrCodeBlock/index.js +1 -1
- package/dist/components/GrCollapse/chunks/GrCollapse-BlW6ybdL.js.map +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-C7ifyQ0R.js → GrColorPicker-p2XH_8cE.js} +2 -2
- package/dist/components/GrColorPicker/chunks/GrColorPicker-p2XH_8cE.js.map +1 -0
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-C5abL2Y7.js → GrCommandPalette-BXbh3q7O.js} +2 -2
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BXbh3q7O.js.map +1 -0
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrCommandPalette/styles.css +1 -1
- package/dist/components/GrConfigProvider/chunks/GrConfigProvider-BF7R3hgg.js.map +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-B9mIU78E.js → GrConfirmDialog-CUP43q__.js} +3 -3
- package/dist/components/GrConfirmDialog/chunks/GrConfirmDialog-CUP43q__.js.map +1 -0
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js → GrContextMenu-ClkVjdcP.js} +1 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-ClkVjdcP.js.map +1 -0
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-C8a9_nCg.js → GrDataTable-CAGgRk27.js} +5 -3
- package/dist/components/GrDataTable/chunks/GrDataTable-CAGgRk27.js.map +1 -0
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDelta/chunks/GrDelta-BZwkgQW-.js.map +1 -1
- package/dist/components/GrDescriptionList/chunks/GrDescriptionList-BMeCgFV3.js.map +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-bOUDYfmh.js → GrDialog-BiMJ9_T4.js} +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-bOUDYfmh.js.map → GrDialog-BiMJ9_T4.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-6s2cQ-_6.js → GrDialogService-BxNK6crD.js} +4 -4
- package/dist/components/GrDialogService/chunks/GrDialogService-BxNK6crD.js.map +1 -0
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDivider/chunks/GrDivider-BXavGcMX.js.map +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-B7PiAcbU.js → GrDrawer-Ce2l3zaG.js} +6 -5
- package/dist/components/GrDrawer/chunks/GrDrawer-Ce2l3zaG.js.map +1 -0
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js.map +1 -1
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenu-GOzuAXsi.js.map +1 -1
- package/dist/components/GrDropdownMenu/chunks/GrDropdownMenuList-B33ZzwfB.js.map +1 -1
- package/dist/components/GrFilePreview/chunks/GrFilePreview-C9Z7hMK8.js.map +1 -1
- package/dist/components/GrFileUpload/chunks/GrFileUpload-NsHkRRIa.js.map +1 -1
- package/dist/components/GrForm/chunks/GrForm-CrnfCsgj.js.map +1 -1
- package/dist/components/GrFormField/chunks/GrFormField-DSylU7IU.js.map +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-CYtXU9HV.js → GrFormFile-B1jGZyIj.js} +2 -2
- package/dist/components/GrFormFile/chunks/GrFormFile-B1jGZyIj.js.map +1 -0
- package/dist/components/GrFormFile/index.js +1 -1
- package/dist/components/GrFormFile/styles.css +1 -1
- package/dist/components/GrIcon/chunks/GrIcon-Dmf0c9ba.js.map +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CyVN6Zpc.js → GrImageViewer-CG4vUBg_.js} +4 -3
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CG4vUBg_.js.map +1 -0
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInput/chunks/GrInput-BLywRZUN.js.map +1 -1
- package/dist/components/GrInputTag/chunks/GrInputTag-Cj4A9Xgs.js.map +1 -1
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-Cy2bknHO.js → GrJsonViewer-BCZ7lE2k.js} +2 -2
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-BCZ7lE2k.js.map +1 -0
- package/dist/components/GrJsonViewer/index.js +1 -1
- package/dist/components/GrKbd/chunks/GrKbd-D2ga1H9i.js.map +1 -1
- package/dist/components/GrLink/chunks/GrLink-DYXw3W9w.js.map +1 -1
- package/dist/components/GrList/chunks/{GrList-DmT4qzLO.js → GrList-B_CRJB4c.js} +1 -1
- package/dist/components/GrList/chunks/GrList-B_CRJB4c.js.map +1 -0
- package/dist/components/GrList/index.js +1 -1
- package/dist/components/GrList/styles.css +1 -1
- package/dist/components/GrModal/chunks/{GrModal-CCoWZuIl.js → GrModal-D0mf1SAx.js} +3 -2
- package/dist/components/GrModal/chunks/GrModal-D0mf1SAx.js.map +1 -0
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-C5EjMKaC.js → GrNavbar-_rtHKwTf.js} +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-C5EjMKaC.js.map → GrNavbar-_rtHKwTf.js.map} +1 -1
- package/dist/components/GrNavbar/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/GrNumberInput-DQ59YICu.js.map +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-Cw3R21dp.js → GrPagination-B_kAhoAV.js} +2 -2
- package/dist/components/GrPagination/chunks/{GrPagination-Cw3R21dp.js.map → GrPagination-B_kAhoAV.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js → GrPopover-faJanmGs.js} +1 -1
- package/dist/components/GrPopover/chunks/GrPopover-faJanmGs.js.map +1 -0
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DLcl1xiq.js → GrPromptDialog-C6QGC7QJ.js} +4 -4
- package/dist/components/GrPromptDialog/chunks/GrPromptDialog-C6QGC7QJ.js.map +1 -0
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrRadio/chunks/{GrRadio-C_rESc5g.js → GrRadio-Aq3v8XgG.js} +1 -1
- package/dist/components/GrRadio/chunks/GrRadio-Aq3v8XgG.js.map +1 -0
- package/dist/components/GrRadio/index.js +2 -2
- package/dist/components/GrRadioGroup/chunks/{GrRadioGroup-CdQO4G-U.js → GrRadioGroup-y9LVdTWh.js} +1 -1
- package/dist/components/GrRadioGroup/chunks/GrRadioGroup-y9LVdTWh.js.map +1 -0
- package/dist/components/GrRadioGroup/index.js +1 -1
- package/dist/components/GrRating/chunks/GrRating-IEiTOdnF.js.map +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-C_skObtv.js → GrResponseErrorBanner-CBVRpgIG.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-C_skObtv.js.map → GrResponseErrorBanner-CBVRpgIG.js.map} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-DudGc9LO.js → GrResponseErrorBanner-DMeOWlzj.js} +1 -1
- package/dist/components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-DMeOWlzj.js.map +1 -0
- package/dist/components/GrResponseErrorBanner/index.js +2 -2
- package/dist/components/GrSegmented/chunks/GrSegmented-CB-VR-yL.js.map +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-BiUrSSxc.js → GrSelect-gCeRFgQo.js} +1 -1
- package/dist/components/GrSelect/chunks/GrSelect-gCeRFgQo.js.map +1 -0
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrSelect/styles.css +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-B8EQIlfW.js → GrSidebar-alkwBUzL.js} +1 -1
- package/dist/components/GrSidebar/chunks/GrSidebar-alkwBUzL.js.map +1 -0
- package/dist/components/GrSidebar/index.js +1 -1
- package/dist/components/GrSkeleton/chunks/GrSkeleton-CgfXxPQ2.js.map +1 -1
- package/dist/components/GrSlider/chunks/{GrSlider-DJfeEl4h.js → GrSlider-DTTrZTHK.js} +3 -3
- package/dist/components/GrSlider/chunks/GrSlider-DTTrZTHK.js.map +1 -0
- package/dist/components/GrSlider/index.js +2 -2
- package/dist/components/GrSortableList/chunks/GrSortableList-HyKX--rO.js.map +1 -1
- package/dist/components/GrSplitter/chunks/GrSplitter-MltjaADn.js.map +1 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-qHVasK37.js.map +1 -1
- package/dist/components/GrSteps/chunks/GrSteps-C2jxpZoN.js.map +1 -1
- package/dist/components/GrSwitch/chunks/{GrSwitch-g_OOSkh7.js → GrSwitch-7Oe5QfQZ.js} +1 -1
- package/dist/components/GrSwitch/chunks/GrSwitch-7Oe5QfQZ.js.map +1 -0
- package/dist/components/GrSwitch/index.js +1 -1
- package/dist/components/GrTabPanels/chunks/GrTabPanels-BZQS8-JM.js.map +1 -1
- package/dist/components/GrTable/chunks/GrTable-Zcdz2St_.js.map +1 -1
- package/dist/components/GrTextarea/chunks/{GrTextarea-KREK75Bu.js → GrTextarea-DinXG12-.js} +10 -10
- package/dist/components/GrTextarea/chunks/GrTextarea-DinXG12-.js.map +1 -0
- package/dist/components/GrTextarea/index.js +1 -1
- package/dist/components/GrTimeline/chunks/GrTimeline-BiHpArsg.js.map +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Dp7-4B8k.js → GrToaster-COXIBKzq.js} +2 -2
- package/dist/components/GrToaster/chunks/GrToaster-COXIBKzq.js.map +1 -0
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrToaster/styles.css +1 -1
- package/dist/components/GrTooltip/chunks/GrTooltip-uYETdbCD.js.map +1 -1
- package/dist/components/GrTree/chunks/{GrTree-DC89I4AP.js → GrTree-Ci6CZfKR.js} +5 -5
- package/dist/components/GrTree/chunks/GrTree-Ci6CZfKR.js.map +1 -0
- package/dist/components/GrTree/index.js +1 -1
- package/dist/components/GrTree/styles.css +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-iJnMl2t9.js → GrTreeSelect-BHwgGJcT.js} +1 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-BHwgGJcT.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +1 -1
- package/dist/composables/useAnnouncer.js.map +1 -1
- package/dist/composables/useComboboxNavigation.js.map +1 -1
- package/dist/composables/useDragGesture.js.map +1 -1
- package/dist/composables/useFloating.js +2 -22
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/composables/useFocusTrap.js.map +1 -1
- package/dist/composables/useOverlayLayer.js +10 -3
- package/dist/composables/useOverlayLayer.js.map +1 -1
- package/dist/composables/useRovingFocus.js.map +1 -1
- package/dist/composables/useTheme.js.map +1 -1
- package/dist/composables/useToast.js.map +1 -1
- package/dist/composables/useVirtualList.js.map +1 -1
- package/dist/directives/autofocus.js.map +1 -1
- package/dist/directives/autosize.js.map +1 -1
- package/dist/directives/clickOutside.js.map +1 -1
- package/dist/directives/dropzone.js.map +1 -1
- package/dist/directives/hotkey.js.map +1 -1
- package/dist/directives/loading.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +37 -37
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/theme-apply.js.map +1 -1
- package/dist/theme.js +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrCodeBlock/tokenizeJson.d.ts +1 -3
- package/dist/types/components/GrImageViewer/GrImageViewer.vue.d.ts +1 -1
- package/dist/types/components/GrInput/GrInput.vue.d.ts +24 -24
- package/dist/types/components/GrSlider/grSliderStyles.d.ts +9 -1
- package/dist/types/components/GrTabPanels/GrTabPanel.vue.d.ts +3 -3
- package/dist/types/composables/internal/overlayStack.d.ts +30 -0
- package/dist/types/composables/internal/useModalOverlay.d.ts +14 -0
- package/dist/types/composables/useOverlayLayer.d.ts +8 -0
- package/dist/vue/index.js.map +1 -1
- package/dist/web-types.json +1 -1
- package/package.json +3 -3
- package/dist/chunks/grButtonStyles-B_-YRE__.js.map +0 -1
- package/dist/chunks/grSliderStyles-B5uNxNJq.js.map +0 -1
- package/dist/chunks/overlayStack-C8cfsqXa.js.map +0 -1
- package/dist/chunks/useModalOverlay-Cv5hF63W.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-X6RlmiJQ.js.map +0 -1
- package/dist/components/GrBadge/chunks/GrBadge-BsoocpF-.js.map +0 -1
- package/dist/components/GrButton/chunks/GrButton-Bw2AH4uX.js.map +0 -1
- package/dist/components/GrCodeBlock/chunks/GrCodeBlock-Dkvkr91t.js.map +0 -1
- package/dist/components/GrColorPicker/chunks/GrColorPicker-C7ifyQ0R.js.map +0 -1
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-C5abL2Y7.js.map +0 -1
- package/dist/components/GrConfirmDialog/chunks/GrConfirmDialog-B9mIU78E.js.map +0 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-_FKwQvuQ.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-C8a9_nCg.js.map +0 -1
- package/dist/components/GrDialogService/chunks/GrDialogService-6s2cQ-_6.js.map +0 -1
- package/dist/components/GrDrawer/chunks/GrDrawer-B7PiAcbU.js.map +0 -1
- package/dist/components/GrFormFile/chunks/GrFormFile-CYtXU9HV.js.map +0 -1
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CyVN6Zpc.js.map +0 -1
- package/dist/components/GrJsonViewer/chunks/GrJsonViewer-Cy2bknHO.js.map +0 -1
- package/dist/components/GrList/chunks/GrList-DmT4qzLO.js.map +0 -1
- package/dist/components/GrModal/chunks/GrModal-CCoWZuIl.js.map +0 -1
- package/dist/components/GrPopover/chunks/GrPopover-CofG-Tfe.js.map +0 -1
- package/dist/components/GrPromptDialog/chunks/GrPromptDialog-DLcl1xiq.js.map +0 -1
- package/dist/components/GrRadio/chunks/GrRadio-C_rESc5g.js.map +0 -1
- package/dist/components/GrRadioGroup/chunks/GrRadioGroup-CdQO4G-U.js.map +0 -1
- package/dist/components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-DudGc9LO.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-BiUrSSxc.js.map +0 -1
- package/dist/components/GrSidebar/chunks/GrSidebar-B8EQIlfW.js.map +0 -1
- package/dist/components/GrSlider/chunks/GrSlider-DJfeEl4h.js.map +0 -1
- package/dist/components/GrSwitch/chunks/GrSwitch-g_OOSkh7.js.map +0 -1
- package/dist/components/GrTextarea/chunks/GrTextarea-KREK75Bu.js.map +0 -1
- package/dist/components/GrToaster/chunks/GrToaster-Dp7-4B8k.js.map +0 -1
- package/dist/components/GrTree/chunks/GrTree-DC89I4AP.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-iJnMl2t9.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrForm-CrnfCsgj.js","names":[],"sources":["../../../../src/components/GrForm/modelSnapshot.ts","../../../../src/components/GrForm/GrForm.vue","../../../../src/components/GrForm/GrForm.vue"],"sourcesContent":["/**\n * Снимок модели формы: клон для `resetFields` и отпечаток для `isDirty`.\n *\n * Чистый модуль без Vue: обе операции читают «сквозь» reactive-прокси и\n * тестируются без монтирования.\n *\n * Почему не `JSON.parse(JSON.stringify(...))` и не `structuredClone`: первый\n * теряет `File` (у файла нет ни перечислимых собственных свойств, ни `toJSON`,\n * поэтому в снимок попадал `{}`), второй падает на прокси с `DataCloneError`.\n */\n\n/** `File` и прочие `Blob` — двоичные значения, для которых клон бессмыслен. */\nfunction isBinary(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob\n}\n\nfunction cloneValue(value: unknown): unknown {\n if (typeof value === 'function' || typeof value === 'symbol') return undefined\n if (value === null || typeof value !== 'object') return value\n\n // По ссылке: содержимое файла клонировать нечем, а идентичность как раз и\n // нужна — `resetFields` обязан вернуть тот самый файл, а не его подобие.\n if (isBinary(value)) return value\n\n if (Array.isArray(value)) {\n // Как у `JSON.stringify`: не-сериализуемый элемент массива становится `null`,\n // иначе сброс сдвинул бы индексы.\n return value.map((item) => {\n const cloned = cloneValue(item)\n return cloned === undefined ? null : cloned\n })\n }\n\n // `Date` и прочие носители `toJSON` ведут себя ровно как раньше.\n const toJSON = (value as { toJSON?: unknown }).toJSON\n if (typeof toJSON === 'function') return cloneValue((value as { toJSON: () => unknown }).toJSON())\n\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) {\n const cloned = cloneValue(item)\n if (cloned !== undefined) out[key] = cloned\n }\n return out\n}\n\n/** Клон значения модели: JSON-семантика плюс сохранённые по ссылке файлы. */\nexport function cloneModelValue<T>(value: T): T {\n return value === undefined ? value : (cloneValue(value) as T)\n}\n\n/**\n * Отпечаток файла для сравнения. Тождество здесь не годится: `resetFields`\n * кладёт в модель клон снимка, и сравнение по ссылке считало бы форму грязной\n * сразу после сброса. Тройки «имя, размер, время» достаточно, чтобы отличить\n * один выбранный файл от другого.\n */\nfunction binaryFingerprint(value: Blob): string {\n if (typeof File !== 'undefined' && value instanceof File)\n return `gr-file:${value.name}:${value.size}:${value.lastModified}`\n\n return `gr-blob:${value.size}:${value.type}`\n}\n\n/**\n * Строка, по которой сравниваются модель и снимок. Без неё любой `File`\n * сериализовался в `{}`, и «выбрал документ» было неотличимо от «не трогал».\n */\nexport function modelFingerprint(value: unknown): string {\n return JSON.stringify(value, (_key, item) => isBinary(item) ? binaryFingerprint(item) : item) ?? ''\n}\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\n/**\n * Вызов опциональный: `scrollIntoView` есть в любом браузере, но не в jsdom —\n * тот не считает layout и метода не реализует вовсе. Без `?.` любой тест\n * потребителя, где форма не прошла валидацию, падал бы необработанным отказом\n * промиса, ничего при этом не проверяя про прокрутку.\n */\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n\ndefineSlots<{\n /**\n * Поля формы. Слот-пропы повторяют публичный API инстанса: форма в шаблоне\n * доступна без `ref`, а значит и без `nextTick` после монтирования.\n */\n default?: (props: {\n validate: () => Promise<boolean>\n errors: Record<string, string | undefined>\n isDirty: boolean\n isValid: boolean\n validatingFields: Set<string>\n resetFields: (names?: string | string[]) => void\n setSnapshot: (model?: Record<string, unknown>) => void\n }) => any\n}>()\n\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\n/**\n * Вызов опциональный: `scrollIntoView` есть в любом браузере, но не в jsdom —\n * тот не считает layout и метода не реализует вовсе. Без `?.` любой тест\n * потребителя, где форма не прошла валидацию, падал бы необработанным отказом\n * промиса, ничего при этом не проверяя про прокрутку.\n */\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n\ndefineSlots<{\n /**\n * Поля формы. Слот-пропы повторяют публичный API инстанса: форма в шаблоне\n * доступна без `ref`, а значит и без `nextTick` после монтирования.\n */\n default?: (props: {\n validate: () => Promise<boolean>\n errors: Record<string, string | undefined>\n isDirty: boolean\n isValid: boolean\n validatingFields: Set<string>\n resetFields: (names?: string | string[]) => void\n setSnapshot: (model?: Record<string, unknown>) => void\n }) => any\n}>()\n\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrForm-CrnfCsgj.js","names":[],"sources":["../../../../src/components/GrForm/modelSnapshot.ts","../../../../src/components/GrForm/GrForm.vue","../../../../src/components/GrForm/GrForm.vue"],"sourcesContent":["/**\n * Снимок модели формы: клон для `resetFields` и отпечаток для `isDirty`.\n *\n * Чистый модуль без Vue: обе операции читают «сквозь» reactive-прокси и\n * тестируются без монтирования.\n *\n * Почему не `JSON.parse(JSON.stringify(...))` и не `structuredClone`: первый\n * теряет `File` (у файла нет ни перечислимых собственных свойств, ни `toJSON`,\n * поэтому в снимок попадал `{}`), второй падает на прокси с `DataCloneError`.\n */\n\n/** `File` и прочие `Blob` — двоичные значения, для которых клон бессмыслен. */\nfunction isBinary(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob\n}\n\nfunction cloneValue(value: unknown): unknown {\n if (typeof value === 'function' || typeof value === 'symbol')\n return undefined\n if (value === null || typeof value !== 'object')\n return value\n\n // По ссылке: содержимое файла клонировать нечем, а идентичность как раз и\n // нужна — `resetFields` обязан вернуть тот самый файл, а не его подобие.\n if (isBinary(value))\n return value\n\n if (Array.isArray(value)) {\n // Как у `JSON.stringify`: не-сериализуемый элемент массива становится `null`,\n // иначе сброс сдвинул бы индексы.\n return value.map((item) => {\n const cloned = cloneValue(item)\n return cloned === undefined ? null : cloned\n })\n }\n\n // `Date` и прочие носители `toJSON` ведут себя ровно как раньше.\n const toJSON = (value as { toJSON?: unknown }).toJSON\n if (typeof toJSON === 'function')\n return cloneValue((value as { toJSON: () => unknown }).toJSON())\n\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) {\n const cloned = cloneValue(item)\n if (cloned !== undefined)\n out[key] = cloned\n }\n return out\n}\n\n/** Клон значения модели: JSON-семантика плюс сохранённые по ссылке файлы. */\nexport function cloneModelValue<T>(value: T): T {\n return value === undefined ? value : (cloneValue(value) as T)\n}\n\n/**\n * Отпечаток файла для сравнения. Тождество здесь не годится: `resetFields`\n * кладёт в модель клон снимка, и сравнение по ссылке считало бы форму грязной\n * сразу после сброса. Тройки «имя, размер, время» достаточно, чтобы отличить\n * один выбранный файл от другого.\n */\nfunction binaryFingerprint(value: Blob): string {\n if (typeof File !== 'undefined' && value instanceof File)\n return `gr-file:${value.name}:${value.size}:${value.lastModified}`\n\n return `gr-blob:${value.size}:${value.type}`\n}\n\n/**\n * Строка, по которой сравниваются модель и снимок. Без неё любой `File`\n * сериализовался в `{}`, и «выбрал документ» было неотличимо от «не трогал».\n */\nexport function modelFingerprint(value: unknown): string {\n return JSON.stringify(value, (_key, item) => isBinary(item) ? binaryFingerprint(item) : item) ?? ''\n}\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name)))\n set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length)\n return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur)\n return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length)\n return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError)\n scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length)\n return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot)\n setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function')\n removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\n/**\n * Вызов опциональный: `scrollIntoView` есть в любом браузере, но не в jsdom —\n * тот не считает layout и метода не реализует вовсе. Без `?.` любой тест\n * потребителя, где форма не прошла валидацию, падал бы необработанным отказом\n * промиса, ничего при этом не проверяя про прокрутку.\n */\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el)\n return\n el.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first)\n return\n first.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange)\n void validateField(name, 'change')\n else if (errors.value[name])\n void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required)\n next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message)\n failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n\ndefineSlots<{\n /**\n * Поля формы. Слот-пропы повторяют публичный API инстанса: форма в шаблоне\n * доступна без `ref`, а значит и без `nextTick` после монтирования.\n */\n default?: (props: {\n validate: () => Promise<boolean>\n errors: Record<string, string | undefined>\n isDirty: boolean\n isValid: boolean\n validatingFields: Set<string>\n resetFields: (names?: string | string[]) => void\n setSnapshot: (model?: Record<string, unknown>) => void\n }) => any\n}>()\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name)))\n set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length)\n return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur)\n return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length)\n return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError)\n scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length)\n return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot)\n setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function')\n removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\n/**\n * Вызов опциональный: `scrollIntoView` есть в любом браузере, но не в jsdom —\n * тот не считает layout и метода не реализует вовсе. Без `?.` любой тест\n * потребителя, где форма не прошла валидацию, падал бы необработанным отказом\n * промиса, ничего при этом не проверяя про прокрутку.\n */\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el)\n return\n el.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first)\n return\n first.scrollIntoView?.({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange)\n void validateField(name, 'change')\n else if (errors.value[name])\n void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required)\n next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message)\n failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n\ndefineSlots<{\n /**\n * Поля формы. Слот-пропы повторяют публичный API инстанса: форма в шаблоне\n * доступна без `ref`, а значит и без `nextTick` после монтирования.\n */\n default?: (props: {\n validate: () => Promise<boolean>\n errors: Record<string, string | undefined>\n isDirty: boolean\n isValid: boolean\n validatingFields: Set<string>\n resetFields: (names?: string | string[]) => void\n setSnapshot: (model?: Record<string, unknown>) => void\n }) => any\n}>()\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrFormField-DSylU7IU.js","names":[],"sources":["../../../../src/components/GrFormField/GrFormField.vue","../../../../src/components/GrFormField/GrFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useId, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormContext } from '../GrForm/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_FIELD_KEY } from './context'\nimport {\n controlColumnClass,\n errorBaseClass,\n errorTexts,\n fieldGaps,\n hintBaseClass,\n hintTexts,\n labelBaseClass,\n labelInlineClass,\n labelTexts,\n requiredMarkClass,\n rootColumnClass,\n rootRowClass,\n type GrFormFieldLabelPosition,\n type GrFormFieldSize,\n} from './grFormFieldStyles'\n\nexport interface GrFormFieldProps {\n label?: string\n /**\n * Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри\n * `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по\n * этому имени, а потеря фокуса триггерит валидацию поля.\n */\n name?: string\n /** Явный id контрола. Если не задан — генерируется автоматически. */\n forId?: string\n /**\n * Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной\n * режим без формы тоже здесь. Массив нужен там, где источник ошибок один,\n * а претензий несколько: ответ сервера, валидация файла.\n */\n error?: string | string[]\n /** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */\n hint?: string\n /** Помечает поле обязательным (маркер `*` + `aria-required` у контрола). */\n required?: boolean\n /** Всё поле только для чтения: контролы внутри перестают редактироваться. */\n readonly?: boolean\n /** Поле недоступно. Складывается с `disabled` формы по «или». */\n disabled?: boolean\n /**\n * Показывать текст ошибки. `false` — поле остаётся невалидным для контрола\n * и AT (`aria-invalid`), но сообщение не занимает места: так делают в\n * плотных таблицах-формах, где ошибка объясняется сводкой сверху.\n */\n showMessage?: boolean\n /** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */\n size?: GrFormFieldSize\n /** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */\n labelPosition?: GrFormFieldLabelPosition\n /** Ширина колонки подписи при `labelPosition=\"start\"`. Число — пиксели. */\n labelWidth?: string | number\n labelClass?: LabelClass\n}\n\nimport './defaults'\n\ntype LabelClass = string | string[] | Record<string, boolean>\n\nconst props = withDefaults(\n defineProps<GrFormFieldProps>(),\n {\n label: undefined,\n name: undefined,\n forId: undefined,\n error: undefined,\n hint: undefined,\n required: false,\n readonly: false,\n disabled: false,\n showMessage: true,\n size: undefined,\n labelPosition: undefined,\n labelWidth: undefined,\n labelClass: undefined,\n },\n)\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormField' })\nconst labelPosition = useGrComponentProp('GrFormField', 'labelPosition', () => props.labelPosition, 'top')\nconst isInlineLabel = computed(() => labelPosition.value === 'start')\n\nconst rootClass = computed(() => [\n isInlineLabel.value ? rootRowClass : rootColumnClass,\n fieldGaps[resolvedSize.value],\n])\n\n// Подпись сбоку не сжимается и держит заданную ширину: иначе колонки контролов\n// в форме разъезжаются по длине подписей.\nconst labelStyle = computed(() => {\n if (!isInlineLabel.value || props.labelWidth === undefined)\n return undefined\n\n return { width: typeof props.labelWidth === 'number' ? `${props.labelWidth}px` : props.labelWidth }\n})\n\nconst labelClassName = computed(() => [\n labelBaseClass,\n labelTexts[resolvedSize.value],\n isInlineLabel.value ? labelInlineClass : '',\n])\nconst hintClassName = computed(() => [hintBaseClass, hintTexts[resolvedSize.value]])\nconst errorClassName = computed(() => [errorBaseClass, errorTexts[resolvedSize.value]])\n\n// Контекст `GrForm` (если поле внутри формы). Контролы про форму не знают —\n// оркестрация подключается здесь, на уровне поля.\nconst form = useGrFormContext()\nconst { t } = useGranularityTranslations()\nconst boundToForm = computed(() => Boolean(form && props.name && form.hasField(props.name)))\n\n// Автогенерация id: контрол внутри читает его через inject и связывается с\n// лейблом (`for`) без ручного дублирования `forId`.\nconst generatedId = useId()\nconst fieldId = computed(() => props.forId ?? generatedId)\n\nconst errorId = useId()\nconst hintId = useId()\n\n// Ошибки: явный проп `error` имеет приоритет, иначе — из формы по `name`.\n// Форма отдаёт одну строку, проп — строку или массив; внутри всегда список.\nconst resolvedErrors = computed<string[]>(() => {\n if (props.error)\n return (Array.isArray(props.error) ? props.error : [props.error]).filter(Boolean)\n\n if (boundToForm.value && props.name) {\n const fromForm = form!.errors.value[props.name]\n return fromForm ? [fromForm] : []\n }\n\n return []\n})\n\n// Обязательность: проп `required` ИЛИ наличие `required`-правила в форме.\nconst isRequired = computed(() => {\n if (props.required) return true\n if (boundToForm.value && props.name) return form!.requiredFields.value.has(props.name)\n return false\n})\n\n/** Идёт асинхронная проверка этого поля (правило ходит на сервер). */\nconst isValidating = computed(() =>\n Boolean(boundToForm.value && props.name && form?.validatingFields?.value.has(props.name)),\n)\n\nconst validatingText = computed(() => t('gr.form.validating', 'Checking…'))\n\nconst hasLabel = computed(() => Boolean(props.label) || Boolean(slots.label))\nconst hasHint = computed(() => Boolean(props.hint) || Boolean(slots.hint))\nconst hasError = computed(() => resolvedErrors.value.length > 0)\n// Текст можно скрыть, но невалидность поля от этого не исчезает.\nconst showsMessage = computed(() => hasError.value && props.showMessage)\n// Пока идёт проверка, показываем её вместо старой ошибки: ошибка относится к\n// прежнему значению, и оставлять её на экране — врать про текущее.\nconst showsValidating = computed(() => isValidating.value && props.showMessage)\n\n/**\n * `aria-describedby` контрола: подсказка и ошибка.\n *\n * Id ошибки стоит здесь всегда, а не только когда ошибка есть: часть AT не\n * перечитывает описание после смены самого атрибута. Контейнер ошибки при этом\n * тоже живёт в DOM постоянно и меняет только текст — пустой он ничего не\n * добавляет к описанию.\n */\nconst describedById = computed(() => {\n const ids = [hasHint.value ? hintId : null, errorId].filter(Boolean)\n return ids.length ? ids.join(' ') : undefined\n})\n\nconst labelElementId = useId()\nconst labelId = computed(() => (hasLabel.value ? labelElementId : undefined))\n\nprovide(GR_FORM_FIELD_KEY, {\n id: fieldId,\n labelId,\n readonly: computed(() => props.readonly),\n // «Или», как и у контролов: форма вправе выключить поле, у которого своего\n // `disabled` нет.\n disabled: computed(() => props.disabled || Boolean(form?.disabled?.value)),\n describedById,\n invalid: hasError,\n required: isRequired,\n})\n\n// ————— Интеграция с формой: регистрация (scroll-to-error) + валидация по blur.\nconst fieldRootEl = ref<HTMLElement | null>(null)\nlet unregister: (() => void) | undefined\n\nonMounted(async () => {\n if (form && props.name) {\n unregister = form.registerField(props.name, () => fieldRootEl.value, {\n // Обязательность поля обязана дойти до валидации: иначе звёздочка есть,\n // а submit проходит с пустым полем.\n required: () => props.required,\n })\n }\n\n await nextTick()\n warnIfControlMissed()\n})\n\n/**\n * Контракт поля держится на том, что контрол прочитал `useGrFormFieldContext()`\n * и повесил `id` на себя. Не прочитал — `<label for>` указывает в пустоту:\n * клик по подписи ничего не фокусирует, а связи для скринридера нет, и узнать\n * об этом можно только вручную. Дешёвая страховка — предупреждение в dev.\n */\nfunction warnIfControlMissed(): void {\n if (!__GR_DEV__) return\n if (!hasLabel.value) return\n\n const root = fieldRootEl.value\n if (!root) return\n\n const target = Array.from(root.querySelectorAll<HTMLElement>('[id]'))\n .some(element => element.id === fieldId.value)\n\n if (target) return\n\n console.warn(\n `[GrFormField] Подпись «${props.label ?? ''}» ссылается на id \"${fieldId.value}\", `\n + 'которого нет внутри поля. Контрол должен прочитать `useGrFormFieldContext()` '\n + 'и повесить `field.id` на себя — либо задайте `forId` и тот же `id` на контроле.',\n )\n}\nonBeforeUnmount(() => unregister?.())\n\n// `focusout` всплывает от любого фокусируемого контрола внутри поля (native и\n// кастомного) — единый способ поймать blur без правок самих контролов.\n//\n// Но всплывает он и когда фокус переезжает ВНУТРИ поля: с input на его же\n// кнопку очистки, между чекбоксами группы. Такое поле валидировалось бы раньше,\n// чем пользователь его дозаполнил, поэтому blur считается состоявшимся только\n// если фокус ушёл за пределы корня. `relatedTarget: null` (фокус в никуда:\n// клик по body, уход из окна) — тоже уход.\nfunction onFocusOut(event: FocusEvent): void {\n if (!form || !props.name) return\n\n const next = event.relatedTarget as Node | null\n if (next && fieldRootEl.value?.contains(next)) return\n\n void form.validateField(props.name, 'blur')\n}\n\ndefineSlots<{\n /** Контрол поля. */\n default?: () => any\n /** Подпись вместо пропа `label`. */\n label?: () => any\n /** Подсказка под контролом вместо пропа `hint`. */\n hint?: () => any\n /** Текст ошибки вместо стандартного. Получает уже разрешённый список. */\n error?: (props: { errors: string[] }) => any\n}>()\n\n</script>\n\n<template>\n <div\n ref=\"fieldRootEl\"\n data-gr-form-field\n :class=\"rootClass\"\n :aria-busy=\"isValidating ? 'true' : undefined\"\n @focusout=\"onFocusOut\"\n >\n <label\n v-if=\"hasLabel\"\n :id=\"labelId\"\n :for=\"fieldId\"\n :class=\"[labelClassName, props.labelClass]\"\n :style=\"labelStyle\"\n >\n <slot name=\"label\">{{ props.label }}</slot>\n <span v-if=\"isRequired\" data-gr-form-field-required aria-hidden=\"true\" :class=\"requiredMarkClass\">*</span>\n </label>\n\n <!-- Подсказка, контрол и ошибка живут одной колонкой: при подписи сбоку\n они остаются рядом с контролом, а не разъезжаются по строке. -->\n <div data-gr-form-field-control :class=\"[controlColumnClass, fieldGaps[resolvedSize]]\">\n <p\n v-if=\"hasHint\"\n :id=\"hintId\"\n data-gr-form-field-hint\n :class=\"hintClassName\"\n >\n <slot name=\"hint\">\n {{ props.hint }}\n </slot>\n </p>\n\n <slot />\n\n <!-- Пустой контейнер уводится в `sr-only`, чтобы не добавлять пустую\n строку в колонку поля: gap дал бы видимый отступ. -->\n <div\n :id=\"errorId\"\n data-gr-form-field-error\n role=\"alert\"\n :class=\"showsMessage || showsValidating ? errorClassName : 'sr-only'\"\n >\n <span v-if=\"showsValidating\" data-gr-form-field-validating :class=\"hintClassName\">\n {{ validatingText }}\n </span>\n <slot v-else-if=\"showsMessage\" name=\"error\" :errors=\"resolvedErrors\">\n <div\n v-for=\"(message, index) in resolvedErrors\"\n :key=\"index\"\n data-gr-form-field-error-item\n >\n {{ message }}\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useId, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormContext } from '../GrForm/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_FIELD_KEY } from './context'\nimport {\n controlColumnClass,\n errorBaseClass,\n errorTexts,\n fieldGaps,\n hintBaseClass,\n hintTexts,\n labelBaseClass,\n labelInlineClass,\n labelTexts,\n requiredMarkClass,\n rootColumnClass,\n rootRowClass,\n type GrFormFieldLabelPosition,\n type GrFormFieldSize,\n} from './grFormFieldStyles'\n\nexport interface GrFormFieldProps {\n label?: string\n /**\n * Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри\n * `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по\n * этому имени, а потеря фокуса триггерит валидацию поля.\n */\n name?: string\n /** Явный id контрола. Если не задан — генерируется автоматически. */\n forId?: string\n /**\n * Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной\n * режим без формы тоже здесь. Массив нужен там, где источник ошибок один,\n * а претензий несколько: ответ сервера, валидация файла.\n */\n error?: string | string[]\n /** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */\n hint?: string\n /** Помечает поле обязательным (маркер `*` + `aria-required` у контрола). */\n required?: boolean\n /** Всё поле только для чтения: контролы внутри перестают редактироваться. */\n readonly?: boolean\n /** Поле недоступно. Складывается с `disabled` формы по «или». */\n disabled?: boolean\n /**\n * Показывать текст ошибки. `false` — поле остаётся невалидным для контрола\n * и AT (`aria-invalid`), но сообщение не занимает места: так делают в\n * плотных таблицах-формах, где ошибка объясняется сводкой сверху.\n */\n showMessage?: boolean\n /** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */\n size?: GrFormFieldSize\n /** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */\n labelPosition?: GrFormFieldLabelPosition\n /** Ширина колонки подписи при `labelPosition=\"start\"`. Число — пиксели. */\n labelWidth?: string | number\n labelClass?: LabelClass\n}\n\nimport './defaults'\n\ntype LabelClass = string | string[] | Record<string, boolean>\n\nconst props = withDefaults(\n defineProps<GrFormFieldProps>(),\n {\n label: undefined,\n name: undefined,\n forId: undefined,\n error: undefined,\n hint: undefined,\n required: false,\n readonly: false,\n disabled: false,\n showMessage: true,\n size: undefined,\n labelPosition: undefined,\n labelWidth: undefined,\n labelClass: undefined,\n },\n)\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormField' })\nconst labelPosition = useGrComponentProp('GrFormField', 'labelPosition', () => props.labelPosition, 'top')\nconst isInlineLabel = computed(() => labelPosition.value === 'start')\n\nconst rootClass = computed(() => [\n isInlineLabel.value ? rootRowClass : rootColumnClass,\n fieldGaps[resolvedSize.value],\n])\n\n// Подпись сбоку не сжимается и держит заданную ширину: иначе колонки контролов\n// в форме разъезжаются по длине подписей.\nconst labelStyle = computed(() => {\n if (!isInlineLabel.value || props.labelWidth === undefined)\n return undefined\n\n return { width: typeof props.labelWidth === 'number' ? `${props.labelWidth}px` : props.labelWidth }\n})\n\nconst labelClassName = computed(() => [\n labelBaseClass,\n labelTexts[resolvedSize.value],\n isInlineLabel.value ? labelInlineClass : '',\n])\nconst hintClassName = computed(() => [hintBaseClass, hintTexts[resolvedSize.value]])\nconst errorClassName = computed(() => [errorBaseClass, errorTexts[resolvedSize.value]])\n\n// Контекст `GrForm` (если поле внутри формы). Контролы про форму не знают —\n// оркестрация подключается здесь, на уровне поля.\nconst form = useGrFormContext()\nconst { t } = useGranularityTranslations()\nconst boundToForm = computed(() => Boolean(form && props.name && form.hasField(props.name)))\n\n// Автогенерация id: контрол внутри читает его через inject и связывается с\n// лейблом (`for`) без ручного дублирования `forId`.\nconst generatedId = useId()\nconst fieldId = computed(() => props.forId ?? generatedId)\n\nconst errorId = useId()\nconst hintId = useId()\n\n// Ошибки: явный проп `error` имеет приоритет, иначе — из формы по `name`.\n// Форма отдаёт одну строку, проп — строку или массив; внутри всегда список.\nconst resolvedErrors = computed<string[]>(() => {\n if (props.error)\n return (Array.isArray(props.error) ? props.error : [props.error]).filter(Boolean)\n\n if (boundToForm.value && props.name) {\n const fromForm = form!.errors.value[props.name]\n return fromForm ? [fromForm] : []\n }\n\n return []\n})\n\n// Обязательность: проп `required` ИЛИ наличие `required`-правила в форме.\nconst isRequired = computed(() => {\n if (props.required) return true\n if (boundToForm.value && props.name) return form!.requiredFields.value.has(props.name)\n return false\n})\n\n/** Идёт асинхронная проверка этого поля (правило ходит на сервер). */\nconst isValidating = computed(() =>\n Boolean(boundToForm.value && props.name && form?.validatingFields?.value.has(props.name)),\n)\n\nconst validatingText = computed(() => t('gr.form.validating', 'Checking…'))\n\nconst hasLabel = computed(() => Boolean(props.label) || Boolean(slots.label))\nconst hasHint = computed(() => Boolean(props.hint) || Boolean(slots.hint))\nconst hasError = computed(() => resolvedErrors.value.length > 0)\n// Текст можно скрыть, но невалидность поля от этого не исчезает.\nconst showsMessage = computed(() => hasError.value && props.showMessage)\n// Пока идёт проверка, показываем её вместо старой ошибки: ошибка относится к\n// прежнему значению, и оставлять её на экране — врать про текущее.\nconst showsValidating = computed(() => isValidating.value && props.showMessage)\n\n/**\n * `aria-describedby` контрола: подсказка и ошибка.\n *\n * Id ошибки стоит здесь всегда, а не только когда ошибка есть: часть AT не\n * перечитывает описание после смены самого атрибута. Контейнер ошибки при этом\n * тоже живёт в DOM постоянно и меняет только текст — пустой он ничего не\n * добавляет к описанию.\n */\nconst describedById = computed(() => {\n const ids = [hasHint.value ? hintId : null, errorId].filter(Boolean)\n return ids.length ? ids.join(' ') : undefined\n})\n\nconst labelElementId = useId()\nconst labelId = computed(() => (hasLabel.value ? labelElementId : undefined))\n\nprovide(GR_FORM_FIELD_KEY, {\n id: fieldId,\n labelId,\n readonly: computed(() => props.readonly),\n // «Или», как и у контролов: форма вправе выключить поле, у которого своего\n // `disabled` нет.\n disabled: computed(() => props.disabled || Boolean(form?.disabled?.value)),\n describedById,\n invalid: hasError,\n required: isRequired,\n})\n\n// ————— Интеграция с формой: регистрация (scroll-to-error) + валидация по blur.\nconst fieldRootEl = ref<HTMLElement | null>(null)\nlet unregister: (() => void) | undefined\n\nonMounted(async () => {\n if (form && props.name) {\n unregister = form.registerField(props.name, () => fieldRootEl.value, {\n // Обязательность поля обязана дойти до валидации: иначе звёздочка есть,\n // а submit проходит с пустым полем.\n required: () => props.required,\n })\n }\n\n await nextTick()\n warnIfControlMissed()\n})\n\n/**\n * Контракт поля держится на том, что контрол прочитал `useGrFormFieldContext()`\n * и повесил `id` на себя. Не прочитал — `<label for>` указывает в пустоту:\n * клик по подписи ничего не фокусирует, а связи для скринридера нет, и узнать\n * об этом можно только вручную. Дешёвая страховка — предупреждение в dev.\n */\nfunction warnIfControlMissed(): void {\n if (!__GR_DEV__) return\n if (!hasLabel.value) return\n\n const root = fieldRootEl.value\n if (!root) return\n\n const target = Array.from(root.querySelectorAll<HTMLElement>('[id]'))\n .some(element => element.id === fieldId.value)\n\n if (target) return\n\n console.warn(\n `[GrFormField] Подпись «${props.label ?? ''}» ссылается на id \"${fieldId.value}\", `\n + 'которого нет внутри поля. Контрол должен прочитать `useGrFormFieldContext()` '\n + 'и повесить `field.id` на себя — либо задайте `forId` и тот же `id` на контроле.',\n )\n}\nonBeforeUnmount(() => unregister?.())\n\n// `focusout` всплывает от любого фокусируемого контрола внутри поля (native и\n// кастомного) — единый способ поймать blur без правок самих контролов.\n//\n// Но всплывает он и когда фокус переезжает ВНУТРИ поля: с input на его же\n// кнопку очистки, между чекбоксами группы. Такое поле валидировалось бы раньше,\n// чем пользователь его дозаполнил, поэтому blur считается состоявшимся только\n// если фокус ушёл за пределы корня. `relatedTarget: null` (фокус в никуда:\n// клик по body, уход из окна) — тоже уход.\nfunction onFocusOut(event: FocusEvent): void {\n if (!form || !props.name) return\n\n const next = event.relatedTarget as Node | null\n if (next && fieldRootEl.value?.contains(next)) return\n\n void form.validateField(props.name, 'blur')\n}\n\ndefineSlots<{\n /** Контрол поля. */\n default?: () => any\n /** Подпись вместо пропа `label`. */\n label?: () => any\n /** Подсказка под контролом вместо пропа `hint`. */\n hint?: () => any\n /** Текст ошибки вместо стандартного. Получает уже разрешённый список. */\n error?: (props: { errors: string[] }) => any\n}>()\n\n</script>\n\n<template>\n <div\n ref=\"fieldRootEl\"\n data-gr-form-field\n :class=\"rootClass\"\n :aria-busy=\"isValidating ? 'true' : undefined\"\n @focusout=\"onFocusOut\"\n >\n <label\n v-if=\"hasLabel\"\n :id=\"labelId\"\n :for=\"fieldId\"\n :class=\"[labelClassName, props.labelClass]\"\n :style=\"labelStyle\"\n >\n <slot name=\"label\">{{ props.label }}</slot>\n <span v-if=\"isRequired\" data-gr-form-field-required aria-hidden=\"true\" :class=\"requiredMarkClass\">*</span>\n </label>\n\n <!-- Подсказка, контрол и ошибка живут одной колонкой: при подписи сбоку\n они остаются рядом с контролом, а не разъезжаются по строке. -->\n <div data-gr-form-field-control :class=\"[controlColumnClass, fieldGaps[resolvedSize]]\">\n <p\n v-if=\"hasHint\"\n :id=\"hintId\"\n data-gr-form-field-hint\n :class=\"hintClassName\"\n >\n <slot name=\"hint\">\n {{ props.hint }}\n </slot>\n </p>\n\n <slot />\n\n <!-- Пустой контейнер уводится в `sr-only`, чтобы не добавлять пустую\n строку в колонку поля: gap дал бы видимый отступ. -->\n <div\n :id=\"errorId\"\n data-gr-form-field-error\n role=\"alert\"\n :class=\"showsMessage || showsValidating ? errorClassName : 'sr-only'\"\n >\n <span v-if=\"showsValidating\" data-gr-form-field-validating :class=\"hintClassName\">\n {{ validatingText }}\n </span>\n <slot v-else-if=\"showsMessage\" name=\"error\" :errors=\"resolvedErrors\">\n <div\n v-for=\"(message, index) in resolvedErrors\"\n :key=\"index\"\n data-gr-form-field-error-item\n >\n {{ message }}\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrFormField-DSylU7IU.js","names":[],"sources":["../../../../src/components/GrFormField/GrFormField.vue","../../../../src/components/GrFormField/GrFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useId, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormContext } from '../GrForm/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_FIELD_KEY } from './context'\nimport {\n controlColumnClass,\n errorBaseClass,\n errorTexts,\n fieldGaps,\n hintBaseClass,\n hintTexts,\n labelBaseClass,\n labelInlineClass,\n labelTexts,\n requiredMarkClass,\n rootColumnClass,\n rootRowClass,\n type GrFormFieldLabelPosition,\n type GrFormFieldSize,\n} from './grFormFieldStyles'\n\nexport interface GrFormFieldProps {\n label?: string\n /**\n * Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри\n * `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по\n * этому имени, а потеря фокуса триггерит валидацию поля.\n */\n name?: string\n /** Явный id контрола. Если не задан — генерируется автоматически. */\n forId?: string\n /**\n * Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной\n * режим без формы тоже здесь. Массив нужен там, где источник ошибок один,\n * а претензий несколько: ответ сервера, валидация файла.\n */\n error?: string | string[]\n /** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */\n hint?: string\n /** Помечает поле обязательным (маркер `*` + `aria-required` у контрола). */\n required?: boolean\n /** Всё поле только для чтения: контролы внутри перестают редактироваться. */\n readonly?: boolean\n /** Поле недоступно. Складывается с `disabled` формы по «или». */\n disabled?: boolean\n /**\n * Показывать текст ошибки. `false` — поле остаётся невалидным для контрола\n * и AT (`aria-invalid`), но сообщение не занимает места: так делают в\n * плотных таблицах-формах, где ошибка объясняется сводкой сверху.\n */\n showMessage?: boolean\n /** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */\n size?: GrFormFieldSize\n /** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */\n labelPosition?: GrFormFieldLabelPosition\n /** Ширина колонки подписи при `labelPosition=\"start\"`. Число — пиксели. */\n labelWidth?: string | number\n labelClass?: LabelClass\n}\n\nimport './defaults'\n\ntype LabelClass = string | string[] | Record<string, boolean>\n\nconst props = withDefaults(\n defineProps<GrFormFieldProps>(),\n {\n label: undefined,\n name: undefined,\n forId: undefined,\n error: undefined,\n hint: undefined,\n required: false,\n readonly: false,\n disabled: false,\n showMessage: true,\n size: undefined,\n labelPosition: undefined,\n labelWidth: undefined,\n labelClass: undefined,\n },\n)\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormField' })\nconst labelPosition = useGrComponentProp('GrFormField', 'labelPosition', () => props.labelPosition, 'top')\nconst isInlineLabel = computed(() => labelPosition.value === 'start')\n\nconst rootClass = computed(() => [\n isInlineLabel.value ? rootRowClass : rootColumnClass,\n fieldGaps[resolvedSize.value],\n])\n\n// Подпись сбоку не сжимается и держит заданную ширину: иначе колонки контролов\n// в форме разъезжаются по длине подписей.\nconst labelStyle = computed(() => {\n if (!isInlineLabel.value || props.labelWidth === undefined)\n return undefined\n\n return { width: typeof props.labelWidth === 'number' ? `${props.labelWidth}px` : props.labelWidth }\n})\n\nconst labelClassName = computed(() => [\n labelBaseClass,\n labelTexts[resolvedSize.value],\n isInlineLabel.value ? labelInlineClass : '',\n])\nconst hintClassName = computed(() => [hintBaseClass, hintTexts[resolvedSize.value]])\nconst errorClassName = computed(() => [errorBaseClass, errorTexts[resolvedSize.value]])\n\n// Контекст `GrForm` (если поле внутри формы). Контролы про форму не знают —\n// оркестрация подключается здесь, на уровне поля.\nconst form = useGrFormContext()\nconst { t } = useGranularityTranslations()\nconst boundToForm = computed(() => Boolean(form && props.name && form.hasField(props.name)))\n\n// Автогенерация id: контрол внутри читает его через inject и связывается с\n// лейблом (`for`) без ручного дублирования `forId`.\nconst generatedId = useId()\nconst fieldId = computed(() => props.forId ?? generatedId)\n\nconst errorId = useId()\nconst hintId = useId()\n\n// Ошибки: явный проп `error` имеет приоритет, иначе — из формы по `name`.\n// Форма отдаёт одну строку, проп — строку или массив; внутри всегда список.\nconst resolvedErrors = computed<string[]>(() => {\n if (props.error)\n return (Array.isArray(props.error) ? props.error : [props.error]).filter(Boolean)\n\n if (boundToForm.value && props.name) {\n const fromForm = form!.errors.value[props.name]\n return fromForm ? [fromForm] : []\n }\n\n return []\n})\n\n// Обязательность: проп `required` ИЛИ наличие `required`-правила в форме.\nconst isRequired = computed(() => {\n if (props.required)\n return true\n if (boundToForm.value && props.name)\n return form!.requiredFields.value.has(props.name)\n return false\n})\n\n/** Идёт асинхронная проверка этого поля (правило ходит на сервер). */\nconst isValidating = computed(() =>\n Boolean(boundToForm.value && props.name && form?.validatingFields?.value.has(props.name)),\n)\n\nconst validatingText = computed(() => t('gr.form.validating', 'Checking…'))\n\nconst hasLabel = computed(() => Boolean(props.label) || Boolean(slots.label))\nconst hasHint = computed(() => Boolean(props.hint) || Boolean(slots.hint))\nconst hasError = computed(() => resolvedErrors.value.length > 0)\n// Текст можно скрыть, но невалидность поля от этого не исчезает.\nconst showsMessage = computed(() => hasError.value && props.showMessage)\n// Пока идёт проверка, показываем её вместо старой ошибки: ошибка относится к\n// прежнему значению, и оставлять её на экране — врать про текущее.\nconst showsValidating = computed(() => isValidating.value && props.showMessage)\n\n/**\n * `aria-describedby` контрола: подсказка и ошибка.\n *\n * Id ошибки стоит здесь всегда, а не только когда ошибка есть: часть AT не\n * перечитывает описание после смены самого атрибута. Контейнер ошибки при этом\n * тоже живёт в DOM постоянно и меняет только текст — пустой он ничего не\n * добавляет к описанию.\n */\nconst describedById = computed(() => {\n const ids = [hasHint.value ? hintId : null, errorId].filter(Boolean)\n return ids.length ? ids.join(' ') : undefined\n})\n\nconst labelElementId = useId()\nconst labelId = computed(() => (hasLabel.value ? labelElementId : undefined))\n\nprovide(GR_FORM_FIELD_KEY, {\n id: fieldId,\n labelId,\n readonly: computed(() => props.readonly),\n // «Или», как и у контролов: форма вправе выключить поле, у которого своего\n // `disabled` нет.\n disabled: computed(() => props.disabled || Boolean(form?.disabled?.value)),\n describedById,\n invalid: hasError,\n required: isRequired,\n})\n\n// ————— Интеграция с формой: регистрация (scroll-to-error) + валидация по blur.\nconst fieldRootEl = ref<HTMLElement | null>(null)\nlet unregister: (() => void) | undefined\n\nonMounted(async () => {\n if (form && props.name) {\n unregister = form.registerField(props.name, () => fieldRootEl.value, {\n // Обязательность поля обязана дойти до валидации: иначе звёздочка есть,\n // а submit проходит с пустым полем.\n required: () => props.required,\n })\n }\n\n await nextTick()\n warnIfControlMissed()\n})\n\n/**\n * Контракт поля держится на том, что контрол прочитал `useGrFormFieldContext()`\n * и повесил `id` на себя. Не прочитал — `<label for>` указывает в пустоту:\n * клик по подписи ничего не фокусирует, а связи для скринридера нет, и узнать\n * об этом можно только вручную. Дешёвая страховка — предупреждение в dev.\n */\nfunction warnIfControlMissed(): void {\n if (!__GR_DEV__)\n return\n if (!hasLabel.value)\n return\n\n const root = fieldRootEl.value\n if (!root)\n return\n\n const target = Array.from(root.querySelectorAll<HTMLElement>('[id]'))\n .some(element => element.id === fieldId.value)\n\n if (target)\n return\n\n console.warn(\n `[GrFormField] Подпись «${props.label ?? ''}» ссылается на id \"${fieldId.value}\", `\n + 'которого нет внутри поля. Контрол должен прочитать `useGrFormFieldContext()` '\n + 'и повесить `field.id` на себя — либо задайте `forId` и тот же `id` на контроле.',\n )\n}\nonBeforeUnmount(() => unregister?.())\n\n// `focusout` всплывает от любого фокусируемого контрола внутри поля (native и\n// кастомного) — единый способ поймать blur без правок самих контролов.\n//\n// Но всплывает он и когда фокус переезжает ВНУТРИ поля: с input на его же\n// кнопку очистки, между чекбоксами группы. Такое поле валидировалось бы раньше,\n// чем пользователь его дозаполнил, поэтому blur считается состоявшимся только\n// если фокус ушёл за пределы корня. `relatedTarget: null` (фокус в никуда:\n// клик по body, уход из окна) — тоже уход.\nfunction onFocusOut(event: FocusEvent): void {\n if (!form || !props.name)\n return\n\n const next = event.relatedTarget as Node | null\n if (next && fieldRootEl.value?.contains(next))\n return\n\n void form.validateField(props.name, 'blur')\n}\n\ndefineSlots<{\n /** Контрол поля. */\n default?: () => any\n /** Подпись вместо пропа `label`. */\n label?: () => any\n /** Подсказка под контролом вместо пропа `hint`. */\n hint?: () => any\n /** Текст ошибки вместо стандартного. Получает уже разрешённый список. */\n error?: (props: { errors: string[] }) => any\n}>()\n</script>\n\n<template>\n <div\n ref=\"fieldRootEl\"\n data-gr-form-field\n :class=\"rootClass\"\n :aria-busy=\"isValidating ? 'true' : undefined\"\n @focusout=\"onFocusOut\"\n >\n <label\n v-if=\"hasLabel\"\n :id=\"labelId\"\n :for=\"fieldId\"\n :class=\"[labelClassName, props.labelClass]\"\n :style=\"labelStyle\"\n >\n <slot name=\"label\">{{ props.label }}</slot>\n <span v-if=\"isRequired\" data-gr-form-field-required aria-hidden=\"true\" :class=\"requiredMarkClass\">*</span>\n </label>\n\n <!-- Подсказка, контрол и ошибка живут одной колонкой: при подписи сбоку\n они остаются рядом с контролом, а не разъезжаются по строке. -->\n <div data-gr-form-field-control :class=\"[controlColumnClass, fieldGaps[resolvedSize]]\">\n <p\n v-if=\"hasHint\"\n :id=\"hintId\"\n data-gr-form-field-hint\n :class=\"hintClassName\"\n >\n <slot name=\"hint\">\n {{ props.hint }}\n </slot>\n </p>\n\n <slot />\n\n <!-- Пустой контейнер уводится в `sr-only`, чтобы не добавлять пустую\n строку в колонку поля: gap дал бы видимый отступ. -->\n <div\n :id=\"errorId\"\n data-gr-form-field-error\n role=\"alert\"\n :class=\"showsMessage || showsValidating ? errorClassName : 'sr-only'\"\n >\n <span v-if=\"showsValidating\" data-gr-form-field-validating :class=\"hintClassName\">\n {{ validatingText }}\n </span>\n <slot v-else-if=\"showsMessage\" name=\"error\" :errors=\"resolvedErrors\">\n <div\n v-for=\"(message, index) in resolvedErrors\"\n :key=\"index\"\n data-gr-form-field-error-item\n >\n {{ message }}\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useId, useSlots } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormContext } from '../GrForm/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_FIELD_KEY } from './context'\nimport {\n controlColumnClass,\n errorBaseClass,\n errorTexts,\n fieldGaps,\n hintBaseClass,\n hintTexts,\n labelBaseClass,\n labelInlineClass,\n labelTexts,\n requiredMarkClass,\n rootColumnClass,\n rootRowClass,\n type GrFormFieldLabelPosition,\n type GrFormFieldSize,\n} from './grFormFieldStyles'\n\nexport interface GrFormFieldProps {\n label?: string\n /**\n * Имя поля в модели `GrForm` (в т.ч. dot-path `address.city`). Когда поле внутри\n * `GrForm` и задан `name`, ошибка и признак обязательности берутся из формы по\n * этому имени, а потеря фокуса триггерит валидацию поля.\n */\n name?: string\n /** Явный id контрола. Если не задан — генерируется автоматически. */\n forId?: string\n /**\n * Явная ошибка (или несколько). Перекрывает ошибку из `GrForm` — ручной\n * режим без формы тоже здесь. Массив нужен там, где источник ошибок один,\n * а претензий несколько: ответ сервера, валидация файла.\n */\n error?: string | string[]\n /** Подсказка под лейблом/над контролом (можно также через слот `#hint`). */\n hint?: string\n /** Помечает поле обязательным (маркер `*` + `aria-required` у контрола). */\n required?: boolean\n /** Всё поле только для чтения: контролы внутри перестают редактироваться. */\n readonly?: boolean\n /** Поле недоступно. Складывается с `disabled` формы по «или». */\n disabled?: boolean\n /**\n * Показывать текст ошибки. `false` — поле остаётся невалидным для контрола\n * и AT (`aria-invalid`), но сообщение не занимает места: так делают в\n * плотных таблицах-формах, где ошибка объясняется сводкой сверху.\n */\n showMessage?: boolean\n /** Размер подписи, подсказки и ошибки. Не задан — из `GrConfigProvider`, иначе `md`. */\n size?: GrFormFieldSize\n /** Подпись сверху (по умолчанию) или сбоку — для плотных форм. */\n labelPosition?: GrFormFieldLabelPosition\n /** Ширина колонки подписи при `labelPosition=\"start\"`. Число — пиксели. */\n labelWidth?: string | number\n labelClass?: LabelClass\n}\n\nimport './defaults'\n\ntype LabelClass = string | string[] | Record<string, boolean>\n\nconst props = withDefaults(\n defineProps<GrFormFieldProps>(),\n {\n label: undefined,\n name: undefined,\n forId: undefined,\n error: undefined,\n hint: undefined,\n required: false,\n readonly: false,\n disabled: false,\n showMessage: true,\n size: undefined,\n labelPosition: undefined,\n labelWidth: undefined,\n labelClass: undefined,\n },\n)\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormField' })\nconst labelPosition = useGrComponentProp('GrFormField', 'labelPosition', () => props.labelPosition, 'top')\nconst isInlineLabel = computed(() => labelPosition.value === 'start')\n\nconst rootClass = computed(() => [\n isInlineLabel.value ? rootRowClass : rootColumnClass,\n fieldGaps[resolvedSize.value],\n])\n\n// Подпись сбоку не сжимается и держит заданную ширину: иначе колонки контролов\n// в форме разъезжаются по длине подписей.\nconst labelStyle = computed(() => {\n if (!isInlineLabel.value || props.labelWidth === undefined)\n return undefined\n\n return { width: typeof props.labelWidth === 'number' ? `${props.labelWidth}px` : props.labelWidth }\n})\n\nconst labelClassName = computed(() => [\n labelBaseClass,\n labelTexts[resolvedSize.value],\n isInlineLabel.value ? labelInlineClass : '',\n])\nconst hintClassName = computed(() => [hintBaseClass, hintTexts[resolvedSize.value]])\nconst errorClassName = computed(() => [errorBaseClass, errorTexts[resolvedSize.value]])\n\n// Контекст `GrForm` (если поле внутри формы). Контролы про форму не знают —\n// оркестрация подключается здесь, на уровне поля.\nconst form = useGrFormContext()\nconst { t } = useGranularityTranslations()\nconst boundToForm = computed(() => Boolean(form && props.name && form.hasField(props.name)))\n\n// Автогенерация id: контрол внутри читает его через inject и связывается с\n// лейблом (`for`) без ручного дублирования `forId`.\nconst generatedId = useId()\nconst fieldId = computed(() => props.forId ?? generatedId)\n\nconst errorId = useId()\nconst hintId = useId()\n\n// Ошибки: явный проп `error` имеет приоритет, иначе — из формы по `name`.\n// Форма отдаёт одну строку, проп — строку или массив; внутри всегда список.\nconst resolvedErrors = computed<string[]>(() => {\n if (props.error)\n return (Array.isArray(props.error) ? props.error : [props.error]).filter(Boolean)\n\n if (boundToForm.value && props.name) {\n const fromForm = form!.errors.value[props.name]\n return fromForm ? [fromForm] : []\n }\n\n return []\n})\n\n// Обязательность: проп `required` ИЛИ наличие `required`-правила в форме.\nconst isRequired = computed(() => {\n if (props.required)\n return true\n if (boundToForm.value && props.name)\n return form!.requiredFields.value.has(props.name)\n return false\n})\n\n/** Идёт асинхронная проверка этого поля (правило ходит на сервер). */\nconst isValidating = computed(() =>\n Boolean(boundToForm.value && props.name && form?.validatingFields?.value.has(props.name)),\n)\n\nconst validatingText = computed(() => t('gr.form.validating', 'Checking…'))\n\nconst hasLabel = computed(() => Boolean(props.label) || Boolean(slots.label))\nconst hasHint = computed(() => Boolean(props.hint) || Boolean(slots.hint))\nconst hasError = computed(() => resolvedErrors.value.length > 0)\n// Текст можно скрыть, но невалидность поля от этого не исчезает.\nconst showsMessage = computed(() => hasError.value && props.showMessage)\n// Пока идёт проверка, показываем её вместо старой ошибки: ошибка относится к\n// прежнему значению, и оставлять её на экране — врать про текущее.\nconst showsValidating = computed(() => isValidating.value && props.showMessage)\n\n/**\n * `aria-describedby` контрола: подсказка и ошибка.\n *\n * Id ошибки стоит здесь всегда, а не только когда ошибка есть: часть AT не\n * перечитывает описание после смены самого атрибута. Контейнер ошибки при этом\n * тоже живёт в DOM постоянно и меняет только текст — пустой он ничего не\n * добавляет к описанию.\n */\nconst describedById = computed(() => {\n const ids = [hasHint.value ? hintId : null, errorId].filter(Boolean)\n return ids.length ? ids.join(' ') : undefined\n})\n\nconst labelElementId = useId()\nconst labelId = computed(() => (hasLabel.value ? labelElementId : undefined))\n\nprovide(GR_FORM_FIELD_KEY, {\n id: fieldId,\n labelId,\n readonly: computed(() => props.readonly),\n // «Или», как и у контролов: форма вправе выключить поле, у которого своего\n // `disabled` нет.\n disabled: computed(() => props.disabled || Boolean(form?.disabled?.value)),\n describedById,\n invalid: hasError,\n required: isRequired,\n})\n\n// ————— Интеграция с формой: регистрация (scroll-to-error) + валидация по blur.\nconst fieldRootEl = ref<HTMLElement | null>(null)\nlet unregister: (() => void) | undefined\n\nonMounted(async () => {\n if (form && props.name) {\n unregister = form.registerField(props.name, () => fieldRootEl.value, {\n // Обязательность поля обязана дойти до валидации: иначе звёздочка есть,\n // а submit проходит с пустым полем.\n required: () => props.required,\n })\n }\n\n await nextTick()\n warnIfControlMissed()\n})\n\n/**\n * Контракт поля держится на том, что контрол прочитал `useGrFormFieldContext()`\n * и повесил `id` на себя. Не прочитал — `<label for>` указывает в пустоту:\n * клик по подписи ничего не фокусирует, а связи для скринридера нет, и узнать\n * об этом можно только вручную. Дешёвая страховка — предупреждение в dev.\n */\nfunction warnIfControlMissed(): void {\n if (!__GR_DEV__)\n return\n if (!hasLabel.value)\n return\n\n const root = fieldRootEl.value\n if (!root)\n return\n\n const target = Array.from(root.querySelectorAll<HTMLElement>('[id]'))\n .some(element => element.id === fieldId.value)\n\n if (target)\n return\n\n console.warn(\n `[GrFormField] Подпись «${props.label ?? ''}» ссылается на id \"${fieldId.value}\", `\n + 'которого нет внутри поля. Контрол должен прочитать `useGrFormFieldContext()` '\n + 'и повесить `field.id` на себя — либо задайте `forId` и тот же `id` на контроле.',\n )\n}\nonBeforeUnmount(() => unregister?.())\n\n// `focusout` всплывает от любого фокусируемого контрола внутри поля (native и\n// кастомного) — единый способ поймать blur без правок самих контролов.\n//\n// Но всплывает он и когда фокус переезжает ВНУТРИ поля: с input на его же\n// кнопку очистки, между чекбоксами группы. Такое поле валидировалось бы раньше,\n// чем пользователь его дозаполнил, поэтому blur считается состоявшимся только\n// если фокус ушёл за пределы корня. `relatedTarget: null` (фокус в никуда:\n// клик по body, уход из окна) — тоже уход.\nfunction onFocusOut(event: FocusEvent): void {\n if (!form || !props.name)\n return\n\n const next = event.relatedTarget as Node | null\n if (next && fieldRootEl.value?.contains(next))\n return\n\n void form.validateField(props.name, 'blur')\n}\n\ndefineSlots<{\n /** Контрол поля. */\n default?: () => any\n /** Подпись вместо пропа `label`. */\n label?: () => any\n /** Подсказка под контролом вместо пропа `hint`. */\n hint?: () => any\n /** Текст ошибки вместо стандартного. Получает уже разрешённый список. */\n error?: (props: { errors: string[] }) => any\n}>()\n</script>\n\n<template>\n <div\n ref=\"fieldRootEl\"\n data-gr-form-field\n :class=\"rootClass\"\n :aria-busy=\"isValidating ? 'true' : undefined\"\n @focusout=\"onFocusOut\"\n >\n <label\n v-if=\"hasLabel\"\n :id=\"labelId\"\n :for=\"fieldId\"\n :class=\"[labelClassName, props.labelClass]\"\n :style=\"labelStyle\"\n >\n <slot name=\"label\">{{ props.label }}</slot>\n <span v-if=\"isRequired\" data-gr-form-field-required aria-hidden=\"true\" :class=\"requiredMarkClass\">*</span>\n </label>\n\n <!-- Подсказка, контрол и ошибка живут одной колонкой: при подписи сбоку\n они остаются рядом с контролом, а не разъезжаются по строке. -->\n <div data-gr-form-field-control :class=\"[controlColumnClass, fieldGaps[resolvedSize]]\">\n <p\n v-if=\"hasHint\"\n :id=\"hintId\"\n data-gr-form-field-hint\n :class=\"hintClassName\"\n >\n <slot name=\"hint\">\n {{ props.hint }}\n </slot>\n </p>\n\n <slot />\n\n <!-- Пустой контейнер уводится в `sr-only`, чтобы не добавлять пустую\n строку в колонку поля: gap дал бы видимый отступ. -->\n <div\n :id=\"errorId\"\n data-gr-form-field-error\n role=\"alert\"\n :class=\"showsMessage || showsValidating ? errorClassName : 'sr-only'\"\n >\n <span v-if=\"showsValidating\" data-gr-form-field-validating :class=\"hintClassName\">\n {{ validatingText }}\n </span>\n <slot v-else-if=\"showsMessage\" name=\"error\" :errors=\"resolvedErrors\">\n <div\n v-for=\"(message, index) in resolvedErrors\"\n :key=\"index\"\n data-gr-form-field-error-item\n >\n {{ message }}\n </div>\n </slot>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -8,7 +8,7 @@ import { n as useGrFormFieldContext } from "../../../chunks/context-DdNIxa1w.js"
|
|
|
8
8
|
import { useGrFormControl } from "../../../composables/useGrFormControl.js";
|
|
9
9
|
import { t as useFocusWithin } from "../../../chunks/useFocusWithin-BwHE0rzm.js";
|
|
10
10
|
import { t as _plugin_vue_export_helper_default } from "../../../chunks/_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
11
|
-
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-
|
|
11
|
+
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-7GP0zObj.js";
|
|
12
12
|
import { t as useFilePreviews } from "../../../chunks/useFilePreviews-DnwgdyBs.js";
|
|
13
13
|
import { a as iconSizes, c as removeTextSizes, d as textSizes, i as iconOffsets, l as rowGaps, o as previewBaseClass, r as buttonSizes, s as previewSizes, u as stackGaps } from "../../../chunks/config-BcXX7S7B.js";
|
|
14
14
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, markRaw, nextTick, normalizeClass, onBeforeUnmount, openBlock, ref, renderList, renderSlot, toDisplayString, unref, useId, watch, withCtx, withDirectives, withModifiers } from "vue";
|
|
@@ -447,6 +447,6 @@ var GrFormFile_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__P
|
|
|
447
447
|
], 34)), [[unref(vDropzone), dropzone.value]]);
|
|
448
448
|
};
|
|
449
449
|
}
|
|
450
|
-
}), [["__scopeId", "data-v-
|
|
450
|
+
}), [["__scopeId", "data-v-1d61d80c"]]);
|
|
451
451
|
//#endregion
|
|
452
452
|
export { GrFormFile_default as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrFormFile-B1jGZyIj.js","names":[],"sources":["../../../../src/components/GrFormFile/GrFormFile.vue","../../../../src/components/GrFormFile/GrFormFile.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport IconUpload from '~icons/lucide/upload'\nimport IconX from '~icons/lucide/x'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrFormFileSize,\n buttonSizes,\n iconOffsets,\n iconSizes,\n previewBaseClass,\n previewSizes,\n removeTextSizes,\n rowGaps,\n stackGaps,\n textSizes,\n} from './grFormFileStyles'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { vDropzone } from '../../directives'\nimport { acceptValidator, FileValidationError, maxCountValidator, resolveFileValidationMessage, runFileValidators } from '../../fileValidation'\nimport type { FileValidationIssue, FileValidator } from '../../fileValidation'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFormFileError = FileValidationIssue\n\n/**\n * Пропы `GrFormFile` — форма-поле для выбора файлов с встроенной валидацией,\n * drag&drop (через `v-dropzone`) и списком выбранных файлов.\n *\n * Все `*Text`/`placeholder` — i18n-friendly, принимают готовые строки локали.\n */\nexport interface GrFormFileProps {\n modelValue: File | File[] | null\n multiple?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n /** W3C `accept` для `<input type=\"file\">` + sugar к `acceptValidator(...)`. */\n accept?: string\n validators?: FileValidator[]\n /** Максимум файлов в наборе. Лишние не обрезаются молча — набор отбивается ошибкой. */\n limit?: number\n uploadText?: string\n changeText?: string\n removeText?: string\n clearAllText?: string\n placeholder?: string\n /** Размер кнопок, иконок и подписей. */\n size?: GrFormFileSize\n /** Миниатюры для картинок в наборе. Файлы других типов остаются строкой. */\n preview?: boolean\n /** Дополнительная (кастомная) валидация на стороне потребителя. */\n validate?: (files: File[]) => GrFormFileError[] | Promise<GrFormFileError[]>\n /**\n * Контролируемый список ошибок: `v-model:errors`. Задан — показывается он, и\n * внутренняя валидация его не перетирает. Сюда же кладутся ошибки, пришедшие\n * с сервера. Не задан — компонент держит свои ошибки сам.\n */\n errors?: GrFormFileError[]\n}\n\nexport interface GrFormFileEmits {\n (e: 'update:modelValue', value: File | File[] | null): void\n (e: 'change', value: File | File[] | null): void\n (e: 'clear'): void\n /** Результат валидации. Канал один: `validation` дублировал эту же нагрузку. */\n (e: 'update:errors', errors: GrFormFileError[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormFileProps>(),\n {\n multiple: false,\n preview: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n accept: undefined,\n limit: undefined,\n uploadText: undefined,\n changeText: undefined,\n removeText: undefined,\n clearAllText: undefined,\n placeholder: undefined,\n size: undefined,\n validators: undefined,\n validate: undefined,\n errors: undefined,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormFile' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst stackClass = computed(() => stackGaps[resolvedSize.value])\nconst textClass = computed(() => textSizes[resolvedSize.value])\nconst removeTextClass = computed(() => removeTextSizes[resolvedSize.value])\nconst iconOffsetClass = computed(() => iconOffsets[resolvedSize.value])\nconst buttonSize = computed(() => buttonSizes[resolvedSize.value])\nconst iconSize = computed(() => iconSizes[resolvedSize.value])\nconst previewClass = computed(() => [previewSizes[resolvedSize.value], previewBaseClass])\n\nconst emit = defineEmits<GrFormFileEmits>()\ndefineSlots<{\n /** Собственный вывод ошибок вместо списка по умолчанию. */\n error?: (props: { errors: GrFormFileError[] }) => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`. Виджет здесь — кнопка выбора файла, а не нативный\n// `<input type=\"file\">`: он `aria-hidden` и вне таб-порядка, id на нём увёл бы\n// `<label for>` в невидимый элемент.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя ни диалогом, ни\n // перетаскиванием, ни удалением.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedUploadText = computed(() => props.uploadText ?? t('gr.formFile.upload', 'Upload file'))\nconst resolvedChangeText = computed(() => props.changeText ?? t('gr.formFile.change', 'Change file'))\nconst resolvedRemoveText = computed(() => props.removeText ?? t('gr.formFile.remove', 'Remove'))\nconst resolvedClearAllText = computed(() => props.clearAllText ?? t('gr.formFile.clearAll', 'Clear all'))\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.formFile.placeholder', 'No files selected'))\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри зоны фокус ходит между нативным input'ом и кнопками: без границы\n// каждое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\nconst localErrors = ref<GrFormFileError[]>([])\nconst uploadBtnEl = ref<HTMLElement | null>(null)\n\n// Собственные ошибки компонент обязан и объявить (`role=\"alert\"`), и связать с\n// контролом: до этого «уронил файл не того типа» для скринридера выглядело как\n// «ничего не произошло». Ошибка поля из `GrFormField` при этом остаётся —\n// `aria-describedby` держит оба id.\nconst errorsId = useId()\n// Контролируемый список сильнее внутреннего — как `sortKey` у `GrDataTable`.\nconst displayedErrors = computed(() => props.errors ?? localErrors.value)\nconst hasLocalErrors = computed(() => displayedErrors.value.length > 0)\n\n/**\n * `aria-required` и `aria-readonly` роль `button` не поддерживает — axe роняет\n * это как critical `aria-allowed-attr`. Состояния уходят в описание кнопки:\n * иначе они пропали бы для диктора совсем (маркер `*` у `GrFormField`\n * декоративен и скрыт).\n */\nconst stateHintId = useId()\nconst stateHint = computed(() => [\n isRequired.value ? t('gr.form.required', 'This field is required') : undefined,\n isReadonly.value ? t('gr.form.readonly', 'Read only') : undefined,\n].filter(Boolean).join('. ') || undefined)\n\nconst describedByIds = computed(() => {\n return [\n field?.describedById.value,\n hasLocalErrors.value ? errorsId : undefined,\n stateHint.value ? stateHintId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined\n})\n\nconst showsInvalid = computed(() => isInvalid.value || hasLocalErrors.value)\n\nfunction focus(): void {\n uploadBtnEl.value?.focus()\n}\n\nfunction blur(): void {\n uploadBtnEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst files = computed<File[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n return props.modelValue instanceof File ? [props.modelValue] : []\n})\n\nconst hasFiles = computed(() => files.value.length > 0)\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * Отзыв `object URL` привязан к самому набору, а не к местам, где его меняют.\n * Файл уходит из набора десятком путей — кнопка строки, «очистить всё», новый\n * выбор, внешний сброс `v-model`, — и точечные вызовы разъехались бы с первой\n * же новой веткой; blob при этом висел бы в памяти вкладки до перезагрузки.\n */\nwatch(files, (next, prev) => {\n for (const file of prev ?? []) {\n if (!next.includes(file))\n revokePreview(file)\n }\n})\n\nonBeforeUnmount(revokeAllPreviews)\n\n/**\n * Один набор валидаторов на оба пути ввода. Собери его отдельно в `applyFiles`\n * и в `dropzone` — копии разъедутся при первой же правке, и выбор через диалог\n * начнёт вести себя иначе, чем перетаскивание.\n */\nconst effectiveValidators = computed<FileValidator[]>(() => {\n const customValidator: FileValidator | undefined = props.validate\n ? async ({ files }) => (await props.validate?.(files)) ?? []\n : undefined\n\n return [\n acceptValidator(props.accept),\n maxCountValidator(props.limit),\n ...(props.validators ?? []),\n ...(customValidator ? [customValidator] : []),\n ]\n})\n\nfunction clearInputValue() {\n if (!inputRef.value)\n return\n inputRef.value.value = ''\n}\n\nfunction setErrors(next: GrFormFileError[]) {\n localErrors.value = next\n emit('update:errors', next)\n}\n\nfunction clearErrors() {\n if (displayedErrors.value.length === 0)\n return\n setErrors([])\n}\n\nfunction openDialog() {\n if (isLocked.value)\n return\n inputRef.value?.click()\n}\n\nfunction emitModel(nextFiles: File[]) {\n const value: File | File[] | null = props.multiple\n ? nextFiles\n : (nextFiles[0] ?? null)\n\n emit('update:modelValue', value)\n emit('change', value)\n}\n\nasync function applyFiles(nextFiles: File[]) {\n const isPromiseLike = (value: unknown): value is PromiseLike<any> => {\n return !!value && typeof (value as any).then === 'function'\n }\n\n const res = runFileValidators(nextFiles, effectiveValidators.value, {\n source: 'input',\n multiple: props.multiple,\n })\n\n const { files: picked, issues } = isPromiseLike(res) ? await res : res\n\n if (issues.length > 0) {\n setErrors(issues)\n clearInputValue()\n await nextTick()\n return\n }\n\n clearErrors()\n emitModel(picked)\n clearInputValue()\n await nextTick()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value)\n return\n\n const target = event.target as HTMLInputElement | null\n const nextFiles = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n // Reset value so selecting the same file twice triggers change.\n if (target)\n target.value = ''\n await nextTick()\n\n await applyFiles(nextFiles)\n}\n\nfunction clearAll() {\n if (isLocked.value)\n return\n clearErrors()\n clearInputValue()\n emit('clear')\n\n emit('update:modelValue', props.multiple ? [] : null)\n emit('change', props.multiple ? [] : null)\n}\n\nfunction removeAt(index: number) {\n if (isLocked.value)\n return\n if (!props.multiple) {\n clearAll()\n return\n }\n\n const next = files.value.slice()\n next.splice(index, 1)\n\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/** Размер файла в строке списка — как в `GrFileUpload`: без него список не отвечает «сколько». */\nfunction formatFileSize(file: File): string {\n return `${Math.ceil(file.size / 1024)} KB`\n}\n\nfunction issueMessage(issue: GrFormFileError): string {\n const text = resolveFileValidationMessage(issue, t)\n\n // Префикс с именем файла нужен только тем сообщениям, которые сами его не\n // называют, — то есть валидаторам потребителя. Встроенные передают `fileName`\n // параметром и подставляют его в текст, и приписка давала бы «photo.png:\n // Файл «photo.png» …». Признак — наличие параметра, а не поиск подстроки.\n if (issue.fileName && issue.i18nParams?.fileName === undefined)\n return `${issue.fileName}: ${text}`\n\n return text\n}\n\nconst dropzone = computed(() => {\n return {\n enabled: !isLocked.value,\n multiple: props.multiple,\n validators: effectiveValidators.value,\n onFiles: async (dropped: File[]) => {\n // `v-dropzone` уже выполнил валидаторы и нормализацию по `multiple`.\n clearErrors()\n emitModel(dropped)\n clearInputValue()\n await nextTick()\n },\n onError: (error: unknown) => {\n if (error instanceof FileValidationError) {\n setErrors(error.issues)\n }\n else {\n setErrors([\n {\n code: 'accept',\n message: t('gr.formFile.validateError', 'Failed to validate dropped files'),\n },\n ])\n }\n },\n }\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!inputRef.value)\n return\n\n if (!value || (Array.isArray(value) && value.length === 0)) {\n // Позволяет выбрать тот же файл снова после внешнего сброса.\n inputRef.value.value = ''\n }\n },\n)\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-dropzone=\"dropzone\"\n data-gr-form-file\n class=\"rounded-[var(--gr-radius-md)]\"\n :class=\"isDisabled ? 'cursor-not-allowed' : ''\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span v-if=\"stateHint\" :id=\"stateHintId\" class=\"sr-only\">{{ stateHint }}</span>\n\n <input\n ref=\"inputRef\"\n data-gr-form-file-input\n type=\"file\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n class=\"absolute opacity-0 w-px h-px pointer-events-none\"\n :multiple=\"multiple\"\n :accept=\"accept\"\n :disabled=\"isDisabled\"\n @change=\"onInputChange\"\n >\n\n <div class=\"flex flex-col\" :class=\"stackClass\">\n <div class=\"flex flex-wrap items-center\" :class=\"rowClass\">\n <GrButton\n :id=\"fieldId\"\n ref=\"uploadBtnEl\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-upload-btn\n :aria-describedby=\"describedByIds\"\n :aria-invalid=\"showsInvalid ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :disabled=\"isDisabled\"\n @click.prevent=\"openDialog\"\n >\n <GrIcon :size=\"iconSize\">\n <IconUpload />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ hasFiles ? resolvedChangeText : resolvedUploadText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"hasFiles && !multiple && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedRemoveText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"multiple && hasFiles && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-all-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedClearAllText }}</span>\n </GrButton>\n\n <img\n v-if=\"!multiple && hasFiles && previewUrl(files[0]!)\"\n data-gr-form-file-preview\n :src=\"previewUrl(files[0]!)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n v-if=\"!multiple && hasFiles\"\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n data-gr-form-file-single-name\n :title=\"files[0]?.name\"\n >\n {{ files[0]?.name }}\n </span>\n\n <span\n v-if=\"!hasFiles\"\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"textClass\"\n data-gr-form-file-placeholder\n >\n {{ resolvedPlaceholder }}\n </span>\n </div>\n\n <div v-if=\"multiple && hasFiles\" class=\"flex flex-col\" :class=\"stackClass\">\n <div\n v-for=\"(file, index) in files\"\n :key=\"fileKey(file)\"\n class=\"flex items-center gap-2\"\n data-gr-form-file-item\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-form-file-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n :title=\"file.name\"\n data-gr-form-file-item-name\n >\n {{ file.name }}\n </span>\n\n <span\n class=\"text-[var(--gr-muted-fg)] shrink-0\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-size\n >{{ formatFileSize(file) }}</span>\n\n <button\n v-if=\"!isReadonly\"\n type=\"button\"\n class=\"text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)]\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-remove\n :disabled=\"isDisabled\"\n :aria-label=\"t('gr.formFile.removeFile', 'Remove {fileName}', { fileName: file.name })\"\n @click.prevent=\"removeAt(index)\"\n >\n {{ resolvedRemoveText }}\n </button>\n </div>\n </div>\n\n <slot name=\"error\" :errors=\"displayedErrors\">\n <div\n v-if=\"hasLocalErrors\"\n :id=\"errorsId\"\n class=\"text-[var(--gr-danger-text)]\"\n :class=\"textClass\"\n data-gr-form-file-errors\n role=\"alert\"\n >\n <div v-for=\"(e, i) in displayedErrors\" :key=\"i\" data-gr-form-file-error>\n {{ issueMessage(e) }}\n </div>\n </div>\n </slot>\n </div>\n </div>\n</template>\n\n<style scoped>\n[data-gr-form-file].gr-dropzone--over {\n outline: 2px solid var(--gr-ring);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport IconUpload from '~icons/lucide/upload'\nimport IconX from '~icons/lucide/x'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrFormFileSize,\n buttonSizes,\n iconOffsets,\n iconSizes,\n previewBaseClass,\n previewSizes,\n removeTextSizes,\n rowGaps,\n stackGaps,\n textSizes,\n} from './grFormFileStyles'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useFilePreviews } from '../../composables/internal/useFilePreviews'\nimport { vDropzone } from '../../directives'\nimport { acceptValidator, FileValidationError, maxCountValidator, resolveFileValidationMessage, runFileValidators } from '../../fileValidation'\nimport type { FileValidationIssue, FileValidator } from '../../fileValidation'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nexport type GrFormFileError = FileValidationIssue\n\n/**\n * Пропы `GrFormFile` — форма-поле для выбора файлов с встроенной валидацией,\n * drag&drop (через `v-dropzone`) и списком выбранных файлов.\n *\n * Все `*Text`/`placeholder` — i18n-friendly, принимают готовые строки локали.\n */\nexport interface GrFormFileProps {\n modelValue: File | File[] | null\n multiple?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n /** W3C `accept` для `<input type=\"file\">` + sugar к `acceptValidator(...)`. */\n accept?: string\n validators?: FileValidator[]\n /** Максимум файлов в наборе. Лишние не обрезаются молча — набор отбивается ошибкой. */\n limit?: number\n uploadText?: string\n changeText?: string\n removeText?: string\n clearAllText?: string\n placeholder?: string\n /** Размер кнопок, иконок и подписей. */\n size?: GrFormFileSize\n /** Миниатюры для картинок в наборе. Файлы других типов остаются строкой. */\n preview?: boolean\n /** Дополнительная (кастомная) валидация на стороне потребителя. */\n validate?: (files: File[]) => GrFormFileError[] | Promise<GrFormFileError[]>\n /**\n * Контролируемый список ошибок: `v-model:errors`. Задан — показывается он, и\n * внутренняя валидация его не перетирает. Сюда же кладутся ошибки, пришедшие\n * с сервера. Не задан — компонент держит свои ошибки сам.\n */\n errors?: GrFormFileError[]\n}\n\nexport interface GrFormFileEmits {\n (e: 'update:modelValue', value: File | File[] | null): void\n (e: 'change', value: File | File[] | null): void\n (e: 'clear'): void\n /** Результат валидации. Канал один: `validation` дублировал эту же нагрузку. */\n (e: 'update:errors', errors: GrFormFileError[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormFileProps>(),\n {\n multiple: false,\n preview: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n accept: undefined,\n limit: undefined,\n uploadText: undefined,\n changeText: undefined,\n removeText: undefined,\n clearAllText: undefined,\n placeholder: undefined,\n size: undefined,\n validators: undefined,\n validate: undefined,\n errors: undefined,\n },\n)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrFormFile' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst stackClass = computed(() => stackGaps[resolvedSize.value])\nconst textClass = computed(() => textSizes[resolvedSize.value])\nconst removeTextClass = computed(() => removeTextSizes[resolvedSize.value])\nconst iconOffsetClass = computed(() => iconOffsets[resolvedSize.value])\nconst buttonSize = computed(() => buttonSizes[resolvedSize.value])\nconst iconSize = computed(() => iconSizes[resolvedSize.value])\nconst previewClass = computed(() => [previewSizes[resolvedSize.value], previewBaseClass])\n\nconst emit = defineEmits<GrFormFileEmits>()\ndefineSlots<{\n /** Собственный вывод ошибок вместо списка по умолчанию. */\n error?: (props: { errors: GrFormFileError[] }) => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\n// Контекст `GrFormField`. Виджет здесь — кнопка выбора файла, а не нативный\n// `<input type=\"file\">`: он `aria-hidden` и вне таб-порядка, id на нём увёл бы\n// `<label for>` в невидимый элемент.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n // `locked` — «ввод не принимается»: `disabled` или `readonly`. Набор в\n // `readonly` виден и уходит в форму, но поменять его нельзя ни диалогом, ни\n // перетаскиванием, ни удалением.\n locked: isLocked,\n} = useGrFormControl(() => props)\nconst resolvedUploadText = computed(() => props.uploadText ?? t('gr.formFile.upload', 'Upload file'))\nconst resolvedChangeText = computed(() => props.changeText ?? t('gr.formFile.change', 'Change file'))\nconst resolvedRemoveText = computed(() => props.removeText ?? t('gr.formFile.remove', 'Remove'))\nconst resolvedClearAllText = computed(() => props.clearAllText ?? t('gr.formFile.clearAll', 'Clear all'))\nconst resolvedPlaceholder = computed(() => props.placeholder ?? t('gr.formFile.placeholder', 'No files selected'))\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри зоны фокус ходит между нативным input'ом и кнопками: без границы\n// каждое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\nconst localErrors = ref<GrFormFileError[]>([])\nconst uploadBtnEl = ref<HTMLElement | null>(null)\n\n// Собственные ошибки компонент обязан и объявить (`role=\"alert\"`), и связать с\n// контролом: до этого «уронил файл не того типа» для скринридера выглядело как\n// «ничего не произошло». Ошибка поля из `GrFormField` при этом остаётся —\n// `aria-describedby` держит оба id.\nconst errorsId = useId()\n// Контролируемый список сильнее внутреннего — как `sortKey` у `GrDataTable`.\nconst displayedErrors = computed(() => props.errors ?? localErrors.value)\nconst hasLocalErrors = computed(() => displayedErrors.value.length > 0)\n\n/**\n * `aria-required` и `aria-readonly` роль `button` не поддерживает — axe роняет\n * это как critical `aria-allowed-attr`. Состояния уходят в описание кнопки:\n * иначе они пропали бы для диктора совсем (маркер `*` у `GrFormField`\n * декоративен и скрыт).\n */\nconst stateHintId = useId()\nconst stateHint = computed(() => [\n isRequired.value ? t('gr.form.required', 'This field is required') : undefined,\n isReadonly.value ? t('gr.form.readonly', 'Read only') : undefined,\n].filter(Boolean).join('. ') || undefined)\n\nconst describedByIds = computed(() => {\n return [\n field?.describedById.value,\n hasLocalErrors.value ? errorsId : undefined,\n stateHint.value ? stateHintId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined\n})\n\nconst showsInvalid = computed(() => isInvalid.value || hasLocalErrors.value)\n\nfunction focus(): void {\n uploadBtnEl.value?.focus()\n}\n\nfunction blur(): void {\n uploadBtnEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst files = computed<File[]>(() => {\n if (props.multiple) {\n return Array.isArray(props.modelValue) ? props.modelValue : []\n }\n\n return props.modelValue instanceof File ? [props.modelValue] : []\n})\n\nconst hasFiles = computed(() => files.value.length > 0)\n\nconst { fileKey, previewUrl, revokePreview, revokeAllPreviews } = useFilePreviews({\n enabled: () => props.preview,\n})\n\n/**\n * Отзыв `object URL` привязан к самому набору, а не к местам, где его меняют.\n * Файл уходит из набора десятком путей — кнопка строки, «очистить всё», новый\n * выбор, внешний сброс `v-model`, — и точечные вызовы разъехались бы с первой\n * же новой веткой; blob при этом висел бы в памяти вкладки до перезагрузки.\n */\nwatch(files, (next, prev) => {\n for (const file of prev ?? []) {\n if (!next.includes(file))\n revokePreview(file)\n }\n})\n\nonBeforeUnmount(revokeAllPreviews)\n\n/**\n * Один набор валидаторов на оба пути ввода. Собери его отдельно в `applyFiles`\n * и в `dropzone` — копии разъедутся при первой же правке, и выбор через диалог\n * начнёт вести себя иначе, чем перетаскивание.\n */\nconst effectiveValidators = computed<FileValidator[]>(() => {\n const customValidator: FileValidator | undefined = props.validate\n ? async ({ files }) => (await props.validate?.(files)) ?? []\n : undefined\n\n return [\n acceptValidator(props.accept),\n maxCountValidator(props.limit),\n ...(props.validators ?? []),\n ...(customValidator ? [customValidator] : []),\n ]\n})\n\nfunction clearInputValue() {\n if (!inputRef.value)\n return\n inputRef.value.value = ''\n}\n\nfunction setErrors(next: GrFormFileError[]) {\n localErrors.value = next\n emit('update:errors', next)\n}\n\nfunction clearErrors() {\n if (displayedErrors.value.length === 0)\n return\n setErrors([])\n}\n\nfunction openDialog() {\n if (isLocked.value)\n return\n inputRef.value?.click()\n}\n\nfunction emitModel(nextFiles: File[]) {\n const value: File | File[] | null = props.multiple\n ? nextFiles\n : (nextFiles[0] ?? null)\n\n emit('update:modelValue', value)\n emit('change', value)\n}\n\nasync function applyFiles(nextFiles: File[]) {\n const isPromiseLike = (value: unknown): value is PromiseLike<any> => {\n return !!value && typeof (value as any).then === 'function'\n }\n\n const res = runFileValidators(nextFiles, effectiveValidators.value, {\n source: 'input',\n multiple: props.multiple,\n })\n\n const { files: picked, issues } = isPromiseLike(res) ? await res : res\n\n if (issues.length > 0) {\n setErrors(issues)\n clearInputValue()\n await nextTick()\n return\n }\n\n clearErrors()\n emitModel(picked)\n clearInputValue()\n await nextTick()\n}\n\nasync function onInputChange(event: Event) {\n if (isLocked.value)\n return\n\n const target = event.target as HTMLInputElement | null\n const nextFiles = target?.files ? Array.prototype.slice.call(target.files) as File[] : []\n\n // Reset value so selecting the same file twice triggers change.\n if (target)\n target.value = ''\n await nextTick()\n\n await applyFiles(nextFiles)\n}\n\nfunction clearAll() {\n if (isLocked.value)\n return\n clearErrors()\n clearInputValue()\n emit('clear')\n\n emit('update:modelValue', props.multiple ? [] : null)\n emit('change', props.multiple ? [] : null)\n}\n\nfunction removeAt(index: number) {\n if (isLocked.value)\n return\n if (!props.multiple) {\n clearAll()\n return\n }\n\n const next = files.value.slice()\n next.splice(index, 1)\n\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/** Размер файла в строке списка — как в `GrFileUpload`: без него список не отвечает «сколько». */\nfunction formatFileSize(file: File): string {\n return `${Math.ceil(file.size / 1024)} KB`\n}\n\nfunction issueMessage(issue: GrFormFileError): string {\n const text = resolveFileValidationMessage(issue, t)\n\n // Префикс с именем файла нужен только тем сообщениям, которые сами его не\n // называют, — то есть валидаторам потребителя. Встроенные передают `fileName`\n // параметром и подставляют его в текст, и приписка давала бы «photo.png:\n // Файл «photo.png» …». Признак — наличие параметра, а не поиск подстроки.\n if (issue.fileName && issue.i18nParams?.fileName === undefined)\n return `${issue.fileName}: ${text}`\n\n return text\n}\n\nconst dropzone = computed(() => {\n return {\n enabled: !isLocked.value,\n multiple: props.multiple,\n validators: effectiveValidators.value,\n onFiles: async (dropped: File[]) => {\n // `v-dropzone` уже выполнил валидаторы и нормализацию по `multiple`.\n clearErrors()\n emitModel(dropped)\n clearInputValue()\n await nextTick()\n },\n onError: (error: unknown) => {\n if (error instanceof FileValidationError) {\n setErrors(error.issues)\n }\n else {\n setErrors([\n {\n code: 'accept',\n message: t('gr.formFile.validateError', 'Failed to validate dropped files'),\n },\n ])\n }\n },\n }\n})\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!inputRef.value)\n return\n\n if (!value || (Array.isArray(value) && value.length === 0)) {\n // Позволяет выбрать тот же файл снова после внешнего сброса.\n inputRef.value.value = ''\n }\n },\n)\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n v-dropzone=\"dropzone\"\n data-gr-form-file\n class=\"rounded-[var(--gr-radius-md)]\"\n :class=\"isDisabled ? 'cursor-not-allowed' : ''\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span v-if=\"stateHint\" :id=\"stateHintId\" class=\"sr-only\">{{ stateHint }}</span>\n\n <input\n ref=\"inputRef\"\n data-gr-form-file-input\n type=\"file\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n class=\"absolute opacity-0 w-px h-px pointer-events-none\"\n :multiple=\"multiple\"\n :accept=\"accept\"\n :disabled=\"isDisabled\"\n @change=\"onInputChange\"\n >\n\n <div class=\"flex flex-col\" :class=\"stackClass\">\n <div class=\"flex flex-wrap items-center\" :class=\"rowClass\">\n <GrButton\n :id=\"fieldId\"\n ref=\"uploadBtnEl\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-upload-btn\n :aria-describedby=\"describedByIds\"\n :aria-invalid=\"showsInvalid ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :disabled=\"isDisabled\"\n @click.prevent=\"openDialog\"\n >\n <GrIcon :size=\"iconSize\">\n <IconUpload />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ hasFiles ? resolvedChangeText : resolvedUploadText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"hasFiles && !multiple && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedRemoveText }}</span>\n </GrButton>\n\n <GrButton\n v-if=\"multiple && hasFiles && !isReadonly\"\n variant=\"secondary\"\n :size=\"buttonSize\"\n data-gr-form-file-clear-all-btn\n :disabled=\"isDisabled\"\n @click.prevent=\"clearAll\"\n >\n <GrIcon :size=\"iconSize\">\n <IconX />\n </GrIcon>\n <span :class=\"iconOffsetClass\">{{ resolvedClearAllText }}</span>\n </GrButton>\n\n <img\n v-if=\"!multiple && hasFiles && previewUrl(files[0]!)\"\n data-gr-form-file-preview\n :src=\"previewUrl(files[0]!)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n v-if=\"!multiple && hasFiles\"\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n data-gr-form-file-single-name\n :title=\"files[0]?.name\"\n >\n {{ files[0]?.name }}\n </span>\n\n <span\n v-if=\"!hasFiles\"\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"textClass\"\n data-gr-form-file-placeholder\n >\n {{ resolvedPlaceholder }}\n </span>\n </div>\n\n <div v-if=\"multiple && hasFiles\" class=\"flex flex-col\" :class=\"stackClass\">\n <div\n v-for=\"(file, index) in files\"\n :key=\"fileKey(file)\"\n class=\"flex items-center gap-2\"\n data-gr-form-file-item\n >\n <img\n v-if=\"previewUrl(file)\"\n data-gr-form-file-preview\n :src=\"previewUrl(file)\"\n alt=\"\"\n :class=\"previewClass\"\n >\n\n <span\n class=\"text-[var(--gr-muted-fg)] truncate max-w-[240px]\"\n :class=\"textClass\"\n :title=\"file.name\"\n data-gr-form-file-item-name\n >\n {{ file.name }}\n </span>\n\n <span\n class=\"text-[var(--gr-muted-fg)] shrink-0\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-size\n >{{ formatFileSize(file) }}</span>\n\n <button\n v-if=\"!isReadonly\"\n type=\"button\"\n class=\"text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)]\"\n :class=\"removeTextClass\"\n data-gr-form-file-item-remove\n :disabled=\"isDisabled\"\n :aria-label=\"t('gr.formFile.removeFile', 'Remove {fileName}', { fileName: file.name })\"\n @click.prevent=\"removeAt(index)\"\n >\n {{ resolvedRemoveText }}\n </button>\n </div>\n </div>\n\n <slot name=\"error\" :errors=\"displayedErrors\">\n <div\n v-if=\"hasLocalErrors\"\n :id=\"errorsId\"\n class=\"text-[var(--gr-danger-text)]\"\n :class=\"textClass\"\n data-gr-form-file-errors\n role=\"alert\"\n >\n <div v-for=\"(e, i) in displayedErrors\" :key=\"i\" data-gr-form-file-error>\n {{ issueMessage(e) }}\n </div>\n </div>\n </slot>\n </div>\n </div>\n</template>\n\n<style scoped>\n[data-gr-form-file].gr-dropzone--over {\n outline: 2px solid var(--gr-ring);\n outline-offset: 2px;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrFormFile_default } from "./chunks/GrFormFile-
|
|
1
|
+
import { t as GrFormFile_default } from "./chunks/GrFormFile-B1jGZyIj.js";
|
|
2
2
|
import { n as grFormFileSafelist, t as grFormFileConfig } from "../../chunks/config-BcXX7S7B.js";
|
|
3
3
|
export { GrFormFile_default as GrFormFile, GrFormFile_default as default, grFormFileConfig, grFormFileSafelist };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
[data-gr-form-file].gr-dropzone--over[data-v-
|
|
1
|
+
[data-gr-form-file].gr-dropzone--over[data-v-1d61d80c]{outline:2px solid var(--gr-ring);outline-offset:2px}
|