bitboss-ui 3.0.0-beta.48 → 3.0.0-beta.49
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 +24 -7
- package/bin/bitboss-ui-mcp.mjs +15 -3
- package/bin/bitboss-ui.mjs +115 -13
- package/dist/ai/BbAvatar.md +1 -1
- package/dist/ai/BbBreadcrumbs.md +65 -65
- package/dist/ai/BbButton.md +16 -5
- package/dist/ai/BbCheckbox.md +0 -2
- package/dist/ai/BbCheckboxGroup.md +13 -20
- package/dist/ai/BbDatePickerInput.md +4 -2
- package/dist/ai/BbDialog.md +1 -8
- package/dist/ai/BbDropdown.md +231 -220
- package/dist/ai/BbDropdownButton.md +65 -65
- package/dist/ai/BbDropdownGroup.md +2 -2
- package/dist/ai/BbDropzone.md +10 -10
- package/dist/ai/BbForm.md +43 -52
- package/dist/ai/BbHoverCard.md +4 -2
- package/dist/ai/BbNumberInput.md +88 -21
- package/dist/ai/BbPopover.md +29 -23
- package/dist/ai/BbProgress.md +48 -22
- package/dist/ai/BbRadioGroup.md +13 -19
- package/dist/ai/BbSelect.md +16 -36
- package/dist/ai/BbSelectPopover.md +14 -30
- package/dist/ai/BbSmoothHeight.md +0 -2
- package/dist/ai/BbSwitchGroup.md +13 -20
- package/dist/ai/BbTable.md +11 -10
- package/dist/ai/BbTabs.md +106 -105
- package/dist/ai/BbTabsList.md +2 -2
- package/dist/ai/BbTabsPanels.md +1 -1
- package/dist/ai/BbTabsRoot.md +2 -2
- package/dist/ai/BbTooltip.md +24 -10
- package/dist/ai/BbTree.md +14 -11
- package/dist/ai/changelog.json +256 -5
- package/dist/ai/components.json +36 -34
- package/dist/ai/composables/useBbDropdownContext.md +16 -16
- package/dist/ai/composables/useBbFormField.md +5 -1
- package/dist/ai/composables/useBbTabsContext.md +14 -14
- package/dist/ai/guides/agent-contract.md +27 -27
- package/dist/ai/guides/ai-router.md +1 -1
- package/dist/ai/guides/component-picker.md +3 -3
- package/dist/ai/guides/fetch-items-playbook.md +1 -1
- package/dist/ai/guides/icons-policy.md +10 -10
- package/dist/ai/guides/inertia-helpers.md +120 -67
- package/dist/ai/guides/installation-and-plugin-setup.md +43 -15
- package/dist/ai/guides/migration/components/bb-breadcrumbs.md +3 -2
- package/dist/ai/guides/migration/components/bb-checkbox-group.md +16 -16
- package/dist/ai/guides/migration/components/bb-dropdown-button.md +9 -8
- package/dist/ai/guides/migration/components/bb-dropdown.md +15 -14
- package/dist/ai/guides/migration/components/bb-radio-group.md +2 -1
- package/dist/ai/guides/migration/components/bb-select-popover.md +6 -1
- package/dist/ai/guides/migration/components/bb-select.md +10 -2
- package/dist/ai/guides/migration/components/bb-switch-group.md +2 -1
- package/dist/ai/guides/migration/components/bb-tabs.md +9 -8
- package/dist/ai/guides/migration/components/bb-tooltip.md +1 -1
- package/dist/ai/guides/migration/v2-to-v3.md +114 -17
- package/dist/ai/guides/migration/v3-beta-tooling.md +11 -0
- package/dist/ai/guides/options-items-playbook.md +30 -24
- package/dist/ai/guides/passthrough.md +2 -2
- package/dist/ai/guides/preset-components.md +18 -3
- package/dist/ai/guides/typescript-generic-components.md +1 -1
- package/dist/ai/guides/validated-forms.md +11 -22
- package/dist/ai/guides/validation-providers.md +4 -3
- package/dist/ai/helpers/inertia/manifest.json +636 -0
- package/dist/ai/helpers/inertia/types.ts +15 -0
- package/dist/ai/helpers/inertia/useAsyncFn.ts +110 -0
- package/dist/ai/helpers/inertia/useConfirmOnRouteLeave.ts +102 -0
- package/dist/ai/helpers/inertia/useDeleteCmd.ts +30 -0
- package/dist/ai/helpers/inertia/useHasChanged.ts +36 -0
- package/dist/ai/helpers/inertia/useIndexById.ts +23 -0
- package/dist/ai/helpers/inertia/useLazyProp.ts +148 -0
- package/dist/ai/helpers/inertia/useLazyPropGetter.ts +50 -0
- package/dist/ai/helpers/inertia/useLoading.ts +20 -0
- package/dist/ai/helpers/inertia/usePaginatedResponseContext.ts +88 -0
- package/dist/ai/helpers/inertia/usePromiseForm.ts +62 -0
- package/dist/ai/helpers/inertia/useProp.ts +63 -0
- package/dist/ai/helpers/inertia/useQuery.ts +19 -0
- package/dist/ai/helpers/inertia/useQueryObject.ts +116 -0
- package/dist/ai/helpers/inertia/useResourceContext.ts +69 -0
- package/dist/ai/helpers/inertia/useRouteQuery.ts +54 -0
- package/dist/ai/helpers/inertia/useScreenSizes.ts +23 -0
- package/dist/ai/helpers/inertia/useTableContext.ts +34 -0
- package/dist/ai/helpers/inertia/useTableSelectionContext.ts +48 -0
- package/dist/ai/helpers/inertia/useUntil.ts +33 -0
- package/dist/ai/helpers/inertia/utils.ts +24 -0
- package/dist/ai/index.md +6 -6
- package/dist/ai/manifest/components/BbBreadcrumbs.json +10 -6
- package/dist/ai/manifest/components/BbBreadcrumbs.tree.json +1 -1
- package/dist/ai/manifest/components/BbButton.json +1 -1
- package/dist/ai/manifest/components/BbCheckboxGroup.json +2 -2
- package/dist/ai/manifest/components/BbDropdown.json +23 -20
- package/dist/ai/manifest/components/BbDropdown.tree.json +2 -2
- package/dist/ai/manifest/components/BbDropdownButton.json +22 -19
- package/dist/ai/manifest/components/BbDropdownButton.tree.json +1 -1
- package/dist/ai/manifest/components/BbDropdownGroup.json +2 -2
- package/dist/ai/manifest/components/BbForm.json +1 -0
- package/dist/ai/manifest/components/BbNumberInput.json +6 -1
- package/dist/ai/manifest/components/BbNumberInput.tree.json +2 -1
- package/dist/ai/manifest/components/BbPopover.json +1 -0
- package/dist/ai/manifest/components/BbProgress.json +6 -6
- package/dist/ai/manifest/components/BbProgress.tree.json +1 -0
- package/dist/ai/manifest/components/BbRadioGroup.json +2 -2
- package/dist/ai/manifest/components/BbSelect.json +2 -2
- package/dist/ai/manifest/components/BbSelectPopover.json +2 -2
- package/dist/ai/manifest/components/BbSwitchGroup.json +2 -2
- package/dist/ai/manifest/components/BbTabs.json +7 -6
- package/dist/ai/manifest/components/BbTabsList.json +3 -3
- package/dist/ai/manifest/components/BbTabsPanels.json +1 -1
- package/dist/ai/manifest/components/BbTabsRoot.json +5 -4
- package/dist/ai/manifest/components/BbTooltip.json +1 -1
- package/dist/ai/manifest/index.json +2 -2
- package/dist/ai/manifest/meta.json +2 -0
- package/dist/ai/manifest/types.advanced.json +10 -9
- package/dist/ai/manifest/types.api.json +5 -4
- package/dist/ai/recipes/inertia/approvals-inbox.md +16 -18
- package/dist/ai/recipes/inertia/inline-edit-workspace.md +23 -33
- package/dist/ai/recipes/inertia/layout-scaffold.md +16 -16
- package/dist/ai/recipes/inertia/onboarding.md +6 -10
- package/dist/ai/recipes/inertia/ownership-atlas.md +15 -18
- package/dist/ai/recipes/inertia/record-form.md +11 -11
- package/dist/ai/recipes/inertia/records-workspace.md +38 -41
- package/dist/ai/recipes/inertia/settings-workspace.md +26 -38
- package/dist/ai/recipes/inertia/upload-center.md +31 -39
- package/dist/ai/recipes/inertia/wizard-form.md +10 -19
- package/dist/ai/recipes/nuxt/approvals-inbox.md +7 -8
- package/dist/ai/recipes/nuxt/inline-edit-workspace.md +14 -24
- package/dist/ai/recipes/nuxt/layout-scaffold.md +16 -16
- package/dist/ai/recipes/nuxt/onboarding.md +2 -6
- package/dist/ai/recipes/nuxt/ownership-atlas.md +7 -9
- package/dist/ai/recipes/nuxt/record-form.md +5 -5
- package/dist/ai/recipes/nuxt/records-workspace.md +29 -31
- package/dist/ai/recipes/nuxt/settings-workspace.md +22 -34
- package/dist/ai/recipes/nuxt/upload-center.md +22 -29
- package/dist/ai/recipes/nuxt/wizard-form.md +3 -11
- package/dist/ai/recipes/vue/approvals-inbox.md +7 -8
- package/dist/ai/recipes/vue/inline-edit-workspace.md +14 -24
- package/dist/ai/recipes/vue/layout-scaffold.md +16 -16
- package/dist/ai/recipes/vue/onboarding.md +2 -6
- package/dist/ai/recipes/vue/ownership-atlas.md +7 -9
- package/dist/ai/recipes/vue/record-form.md +5 -5
- package/dist/ai/recipes/vue/records-workspace.md +29 -31
- package/dist/ai/recipes/vue/settings-workspace.md +22 -34
- package/dist/ai/recipes/vue/upload-center.md +22 -29
- package/dist/ai/recipes/vue/wizard-form.md +3 -11
- package/dist/ai/source/BbAccordion.md +1 -1
- package/dist/ai/source/BbAlert.md +3 -3
- package/dist/ai/source/BbBadge.md +2 -2
- package/dist/ai/source/BbBadgeButton.md +5 -4
- package/dist/ai/source/BbBaseButton.md +1 -1
- package/dist/ai/source/BbBaseRadioIcon.md +1 -0
- package/dist/ai/source/BbBaseSwitchIcon.md +1 -0
- package/dist/ai/source/BbBreadcrumbs.md +63 -42
- package/dist/ai/source/BbButton.md +71 -33
- package/dist/ai/source/BbCalendar.md +17 -16
- package/dist/ai/source/BbCheckbox.md +3 -2
- package/dist/ai/source/BbCheckboxGroup.md +11 -4
- package/dist/ai/source/BbColorInput.md +10 -9
- package/dist/ai/source/BbConfirm.md +1 -1
- package/dist/ai/source/BbConfirmPortal.md +1 -1
- package/dist/ai/source/BbDatePicker.md +2 -2
- package/dist/ai/source/BbDatePickerInput.md +47 -20
- package/dist/ai/source/BbDialog.md +5 -5
- package/dist/ai/source/BbDropdown.md +230 -158
- package/dist/ai/source/BbDropdownButton.md +19 -12
- package/dist/ai/source/BbDropdownGroup.md +126 -93
- package/dist/ai/source/BbForm.md +25 -1
- package/dist/ai/source/BbIndicator.md +4 -0
- package/dist/ai/source/BbNumberInput.md +855 -19
- package/dist/ai/source/BbOffCanvas.md +27 -11
- package/dist/ai/source/BbPagination.md +3 -3
- package/dist/ai/source/BbPopover.md +152 -8
- package/dist/ai/source/BbPopoverPortal.md +11 -0
- package/dist/ai/source/BbProgress.md +82 -18
- package/dist/ai/source/BbRadioGroup.md +12 -5
- package/dist/ai/source/BbRating.md +11 -9
- package/dist/ai/source/BbSelect.md +16 -10
- package/dist/ai/source/BbSelectPopover.md +29 -23
- package/dist/ai/source/BbSlider.md +12 -6
- package/dist/ai/source/BbSwitch.md +3 -2
- package/dist/ai/source/BbSwitchGroup.md +5 -2
- package/dist/ai/source/BbTable.md +102 -18
- package/dist/ai/source/BbTabs.md +27 -23
- package/dist/ai/source/BbTabsList.md +26 -22
- package/dist/ai/source/BbTabsPanels.md +26 -22
- package/dist/ai/source/BbTabsRoot.md +25 -21
- package/dist/ai/source/BbTag.md +10 -9
- package/dist/ai/source/BbTextInput.md +5 -4
- package/dist/ai/source/BbTextarea.md +6 -5
- package/dist/ai/source/BbTimePicker.md +1 -1
- package/dist/ai/source/BbTimePickerInput.md +46 -9
- package/dist/ai/source/BbToast.md +10 -7
- package/dist/ai/source/BbToastPortal.md +10 -7
- package/dist/ai/source/BbTooltip.md +54 -8
- package/dist/ai/source/BbTree.md +21 -3
- package/dist/ai/source/CommonField.md +6 -4
- package/dist/ai/source/CommonFieldInput.md +15 -10
- package/dist/ai/source/CommonPopover.md +6 -0
- package/dist/ai/source/CommonTimeSelector.md +5 -4
- package/dist/components/BbBadge/BbBadgeButton.vue_vue_type_script_setup_true_lang.js +10 -8
- package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +45 -45
- package/dist/components/BbBreadcrumbs/types.d.ts +25 -17
- package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +84 -71
- package/dist/components/BbButton/toggleRoving.d.ts +6 -0
- package/dist/components/BbButton/toggleRoving.js +45 -0
- package/dist/components/BbButton/types.d.ts +4 -1
- package/dist/components/BbCalendar/BbCalendar.vue_vue_type_script_setup_true_lang.js +178 -178
- package/dist/components/BbCalendar/CalendarMonthPanel.vue_vue_type_script_setup_true_lang.js +15 -15
- package/dist/components/BbCalendar/CalendarYearPanel.vue_vue_type_script_setup_true_lang.js +28 -28
- package/dist/components/BbCalendar/useCalendarGrid.js +141 -139
- package/dist/components/BbCalendar/useDayjsLocale.d.ts +38 -15
- package/dist/components/BbCalendar/useDayjsLocale.js +31 -30
- package/dist/components/BbCheckbox/BbCheckbox.vue_vue_type_script_setup_true_lang.js +48 -48
- package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +81 -80
- package/dist/components/BbCheckboxGroup/types.d.ts +6 -2
- package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +69 -69
- package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +363 -349
- package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +91 -86
- package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +298 -289
- package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +322 -356
- package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +17 -15
- package/dist/components/BbDropdown/DropdownRow.vue.d.ts +40 -0
- package/dist/components/BbDropdown/DropdownRow.vue.js +5 -0
- package/dist/components/BbDropdown/DropdownRow.vue_vue_type_script_setup_true_lang.js +60 -0
- package/dist/components/BbDropdown/normalizeGroups.d.ts +8 -2
- package/dist/components/BbDropdown/normalizeGroups.js +60 -46
- package/dist/components/BbDropdown/types.d.ts +119 -88
- package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +11 -2
- package/dist/components/BbDropdownButton/types.d.ts +2 -2
- package/dist/components/BbForm/BbForm.vue.d.ts +9 -0
- package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +114 -105
- package/dist/components/BbForm/BbFormCustomField.js +1 -1
- package/dist/components/BbForm/bindFormField.js +2 -1
- package/dist/components/BbForm/formFields.d.ts +5 -0
- package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +0 -1
- package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +452 -125
- package/dist/components/BbNumberInput/numberDisplay.d.ts +256 -0
- package/dist/components/BbNumberInput/numberDisplay.js +499 -0
- package/dist/components/BbNumberInput/types.d.ts +54 -1
- package/dist/components/BbNumberInput/warroom/corpus.d.ts +58 -0
- package/dist/components/BbNumberInput/warroom/currency-verify.rows.d.ts +28 -0
- package/dist/components/BbNumberInput/warroom/currency.rows.d.ts +2 -0
- package/dist/components/BbNumberInput/warroom/harness.d.ts +217 -0
- package/dist/components/BbNumberInput/warroom/oracle.d.ts +150 -0
- package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/components/BbPopover/BbPopover.vue.d.ts +1 -0
- package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +207 -164
- package/dist/components/BbPopover/types.d.ts +11 -0
- package/dist/components/BbProgress/BbProgress.vue_vue_type_script_setup_true_lang.js +15 -7
- package/dist/components/BbProgress/types.d.ts +23 -7
- package/dist/components/BbProgress/types.js +1 -1
- package/dist/components/BbRadioGroup/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +102 -101
- package/dist/components/BbRadioGroup/types.d.ts +6 -2
- package/dist/components/BbRating/BbRating.vue_vue_type_script_setup_true_lang.js +63 -63
- package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +156 -154
- package/dist/components/BbSelect/types.d.ts +6 -2
- package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/components/BbSelectPopover/types.d.ts +6 -2
- package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +153 -153
- package/dist/components/BbSwitch/BbSwitch.vue_vue_type_script_setup_true_lang.js +62 -62
- package/dist/components/BbSwitchGroup/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +131 -130
- package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +711 -676
- package/dist/components/BbTabs/BbTabs.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbTabs/BbTabsList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbTabs/types.d.ts +25 -21
- package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +76 -76
- package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +58 -58
- package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +82 -82
- package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +246 -236
- package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +102 -97
- package/dist/components/BbTooltip/tooltipWarmup.d.ts +23 -0
- package/dist/components/BbTooltip/tooltipWarmup.js +7 -0
- package/dist/components/BbTooltip/types.d.ts +7 -2
- package/dist/components/BbTree/BbTree.vue_vue_type_script_setup_true_lang.js +29 -21
- package/dist/components/BbTree/BbTreeLevel.vue_vue_type_script_setup_true_lang.js +10 -7
- package/dist/components/CommonFieldInput/CommonFieldInput.vue_vue_type_script_setup_true_lang.js +37 -36
- package/dist/components/CommonTimeSelector/CommonTimeSelector.vue_vue_type_script_setup_true_lang.js +9 -9
- package/dist/composables/formSubmitLock.d.ts +6 -0
- package/dist/composables/formSubmitLock.js +4 -0
- package/dist/composables/useBaseOptions.d.ts +7 -1
- package/dist/composables/useBaseOptions.js +32 -32
- package/dist/composables/useBbDropdownContext.d.ts +8 -8
- package/dist/composables/useBbFormField.d.ts +9 -0
- package/dist/composables/useBbFormField.js +5 -3
- package/dist/composables/useBbTabsContext.d.ts +10 -10
- package/dist/composables/useBbTabsContext.js +18 -18
- package/dist/composables/useFormBinding.d.ts +11 -0
- package/dist/composables/useFormBinding.js +15 -13
- package/dist/composables/useItemText.d.ts +5 -1
- package/dist/composables/useItemText.js +8 -3
- package/dist/composables/useItemValue.d.ts +5 -1
- package/dist/composables/useItemValue.js +8 -3
- package/dist/composables/useLocale.js +10 -9
- package/dist/deprecation/ai-deprecations.json.d.ts +130 -0
- package/dist/deprecation/ai-deprecations.json.js +1 -1
- package/dist/deprecation/removedProps.js +1 -1
- package/dist/directives/bbDropdown.js +1 -1
- package/dist/i18n/index.d.ts +7 -12
- package/dist/i18n/locales/cs.json +11 -14
- package/dist/i18n/locales/da.json +13 -16
- package/dist/i18n/locales/de-CH.json +137 -0
- package/dist/i18n/locales/de.json +14 -17
- package/dist/i18n/locales/el.json +10 -13
- package/dist/i18n/locales/en-GB.json +137 -0
- package/dist/i18n/locales/en.json +8 -11
- package/dist/i18n/locales/es-MX.json +137 -0
- package/dist/i18n/locales/es.json +16 -19
- package/dist/i18n/locales/fi.json +8 -11
- package/dist/i18n/locales/fr.json +23 -26
- package/dist/i18n/locales/hu.json +8 -11
- package/dist/i18n/locales/it.json +12 -15
- package/dist/i18n/locales/ja.json +13 -16
- package/dist/i18n/locales/ko.json +10 -13
- package/dist/i18n/locales/nb.json +13 -16
- package/dist/i18n/locales/nl.json +21 -24
- package/dist/i18n/locales/pl.json +10 -13
- package/dist/i18n/locales/{pt.json → pt-BR.json} +19 -22
- package/dist/i18n/locales/pt-PT.json +137 -0
- package/dist/i18n/locales/ro.json +22 -25
- package/dist/i18n/locales/ru.json +11 -14
- package/dist/i18n/locales/sv.json +16 -19
- package/dist/i18n/locales/tr.json +14 -17
- package/dist/i18n/locales/uk.json +11 -14
- package/dist/i18n/locales/{zh-cn.json → zh-CN.json} +10 -13
- package/dist/i18n/locales/zh-HK.json +137 -0
- package/dist/i18n/locales/zh-TW.json +137 -0
- package/dist/i18n/regionHints.d.ts +16 -0
- package/dist/i18n/regionHints.js +49 -0
- package/dist/i18n/regions.d.ts +286 -0
- package/dist/i18n/regions.js +265 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/llms-full.txt +1785 -1637
- package/dist/llms-medium.txt +74 -46
- package/dist/llms.txt +7 -7
- package/dist/locale-blueprint.json +16 -13
- package/dist/styles.css +2 -2
- package/dist/types/ptComponentMap.d.ts +1 -1
- package/dist/utilities/functions/slotKey.d.ts +2 -2
- package/dist/utils/versionCheck.js +1 -1
- package/dist/vite-plugin.d.ts +22 -19
- package/dist/vite.js +309 -297
- package/llms.txt +7 -7
- package/package.json +5 -3
- package/scripts/lib/agent-manifest.mjs +2 -1
- package/scripts/lib/eslint-disable.mjs +12 -4
- package/scripts/lib/eslint-plugin.d.ts +5 -1
- package/scripts/lib/eslint-plugin.mjs +187 -6
- package/scripts/lib/helper-canon.mjs +176 -0
- package/scripts/lib/helpers-add.mjs +1303 -0
- package/scripts/lib/local-components.mjs +33 -4
- package/scripts/lib/locale-check.mjs +165 -0
- package/scripts/lib/redundant-item-accessor.mjs +398 -0
- package/scripts/lib/renamed-item-field.mjs +343 -0
- package/scripts/lib/slot-key.mjs +2 -2
- package/scripts/lib/slot-renames.mjs +1 -1
- package/scripts/lib/validate-bb-markup.mjs +123 -56
- package/dist/i18n/index.js +0 -27
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
:loading="loading"
|
|
29
29
|
:name="name"
|
|
30
30
|
:passthrough="passthrough"
|
|
31
|
-
:readonly="
|
|
31
|
+
:readonly="fieldReadonly"
|
|
32
32
|
:reverse="reverse"
|
|
33
33
|
:show-asterisk="showAsterisk"
|
|
34
34
|
:show-hint="showHint"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
:loading="loading"
|
|
60
60
|
:passthrough="passthrough"
|
|
61
61
|
:prepend:icon="props['prepend:icon']"
|
|
62
|
-
:readonly="
|
|
62
|
+
:readonly="fieldReadonly"
|
|
63
63
|
:reserve-append-space="clearable"
|
|
64
64
|
:variant="variant"
|
|
65
65
|
@click:clear="emit('update:modelValue', null)"
|
|
@@ -85,18 +85,19 @@
|
|
|
85
85
|
:aria-disabled="disabled || undefined"
|
|
86
86
|
:aria-invalid="fieldHasErrors || undefined"
|
|
87
87
|
:aria-labelledby="ariaLabelledby"
|
|
88
|
-
:aria-readonly="
|
|
88
|
+
:aria-readonly="fieldReadonly || undefined"
|
|
89
89
|
:aria-valuemax="max !== Number.MAX_SAFE_INTEGER ? max : undefined"
|
|
90
90
|
:aria-valuemin="min !== Number.MIN_SAFE_INTEGER ? min : undefined"
|
|
91
91
|
:aria-valuenow="isNotNil(fieldModel) ? fieldModel : undefined"
|
|
92
|
+
:aria-valuetext="ariaValuetext"
|
|
92
93
|
:autocomplete="autocomplete"
|
|
93
94
|
:autofocus="autofocus"
|
|
94
95
|
class="bb-number-input__input"
|
|
95
96
|
:disabled="disabled"
|
|
96
97
|
inputmode="decimal"
|
|
97
|
-
:name="name"
|
|
98
|
+
:name="disableFormatting ? name : undefined"
|
|
98
99
|
:placeholder="placeholder"
|
|
99
|
-
:readonly="
|
|
100
|
+
:readonly="fieldReadonly"
|
|
100
101
|
:required="fieldRequired"
|
|
101
102
|
:aria-required="fieldAriaRequired"
|
|
102
103
|
role="spinbutton"
|
|
@@ -121,6 +122,18 @@
|
|
|
121
122
|
></slot
|
|
122
123
|
></template>
|
|
123
124
|
</CommonFieldInput>
|
|
125
|
+
<!-- Formatted mode posts the raw period string from here: the visible
|
|
126
|
+
text is the locale's (`10.005,6`). `form` routes from the consumer's
|
|
127
|
+
attributes, like BbSelect's value channel. -->
|
|
128
|
+
<input
|
|
129
|
+
v-if="!disableFormatting && name"
|
|
130
|
+
ref="posted"
|
|
131
|
+
:disabled="disabled"
|
|
132
|
+
:form="(passthrough.controlAttrs as { form?: string }).form"
|
|
133
|
+
:name="name"
|
|
134
|
+
type="hidden"
|
|
135
|
+
:value="submitValue"
|
|
136
|
+
/>
|
|
124
137
|
</template>
|
|
125
138
|
</CommonField>
|
|
126
139
|
</template>
|
|
@@ -143,7 +156,15 @@ import { warnRemovedAttrs } from '@/utilities/functions/warnRemovedAttrs';
|
|
|
143
156
|
* - Supports keyboard stepping with ArrowUp/ArrowDown.
|
|
144
157
|
* - Hands `increase()` / `decrease()` to every slot for step controls.
|
|
145
158
|
*/
|
|
146
|
-
import {
|
|
159
|
+
import {
|
|
160
|
+
computed,
|
|
161
|
+
mergeProps,
|
|
162
|
+
onBeforeUnmount,
|
|
163
|
+
onMounted,
|
|
164
|
+
ref,
|
|
165
|
+
useAttrs,
|
|
166
|
+
watch,
|
|
167
|
+
} from 'vue';
|
|
147
168
|
import CommonField from '../CommonField/CommonField.vue';
|
|
148
169
|
import CommonFieldInput from '../CommonFieldInput/CommonFieldInput.vue';
|
|
149
170
|
import { useConfig } from '@/composables/useConfig';
|
|
@@ -158,6 +179,30 @@ import { clampPrecision } from '@/utilities/functions/clampPrecision';
|
|
|
158
179
|
import { isNil } from '@/utilities/functions/isNil';
|
|
159
180
|
import { isNotNil } from '@/utilities/functions/isNotNil';
|
|
160
181
|
import { vSsrValue } from '@/utilities/misc/ssrValue';
|
|
182
|
+
import { intlLocaleOf } from '../BbCalendar/useDayjsLocale';
|
|
183
|
+
import {
|
|
184
|
+
affixOf,
|
|
185
|
+
applyEdit,
|
|
186
|
+
currencyDigitsOf,
|
|
187
|
+
displayIndexOf,
|
|
188
|
+
formatAffixed,
|
|
189
|
+
isShellOnly,
|
|
190
|
+
numberSymbolsOf,
|
|
191
|
+
PLAIN_SYMBOLS,
|
|
192
|
+
rawAtRest,
|
|
193
|
+
rawIndexOf,
|
|
194
|
+
rawOfNumber,
|
|
195
|
+
readText,
|
|
196
|
+
snapOffset,
|
|
197
|
+
spokenAffixed,
|
|
198
|
+
stripAffix,
|
|
199
|
+
valueOfRaw,
|
|
200
|
+
withPaddedMark,
|
|
201
|
+
writeStrategy,
|
|
202
|
+
type Display,
|
|
203
|
+
type Edit,
|
|
204
|
+
type EditState,
|
|
205
|
+
} from './numberDisplay';
|
|
161
206
|
import {
|
|
162
207
|
BB_NUMBER_INPUT_PT,
|
|
163
208
|
type BbNumberInputProps,
|
|
@@ -166,13 +211,13 @@ import {
|
|
|
166
211
|
} from './types';
|
|
167
212
|
|
|
168
213
|
defineOptions({ inheritAttrs: false });
|
|
169
|
-
// `min` / `max` / `step`
|
|
170
|
-
// `aria-
|
|
171
|
-
//
|
|
214
|
+
// `min` / `max` / `step` drive the clamp and step math and `aria-valuemin` /
|
|
215
|
+
// `aria-valuemax` (the SAFE_INTEGER sentinels stay silent), so they are
|
|
216
|
+
// defaulted here, before anything reads them. `maxPrecision` / `minPrecision`
|
|
217
|
+
// default below: with `currency` they follow the currency's digits.
|
|
172
218
|
const declaredProps = withDefaults(defineProps<BbNumberInputProps>(), {
|
|
173
219
|
autocomplete: 'off',
|
|
174
220
|
max: Number.MAX_SAFE_INTEGER,
|
|
175
|
-
maxPrecision: 8,
|
|
176
221
|
min: Number.MIN_SAFE_INTEGER,
|
|
177
222
|
step: 1,
|
|
178
223
|
});
|
|
@@ -188,6 +233,7 @@ const {
|
|
|
188
233
|
required: fieldRequired,
|
|
189
234
|
ariaRequired: fieldAriaRequired,
|
|
190
235
|
model: fieldModel,
|
|
236
|
+
readonly: fieldReadonly,
|
|
191
237
|
} = useFormBinding(declaredProps, declaredEmit, {
|
|
192
238
|
componentName: 'BbNumberInput',
|
|
193
239
|
identifierFallback: () => declaredProps.label,
|
|
@@ -228,6 +274,612 @@ const passthrough = usePassthrough({
|
|
|
228
274
|
|
|
229
275
|
const input = ref<HTMLInputElement | null>(null);
|
|
230
276
|
|
|
277
|
+
// --- Q60.25: the locale display (owner 2026-10-07) -------------------------
|
|
278
|
+
//
|
|
279
|
+
// The field shows its number the locale's way while the user types
|
|
280
|
+
// (`10.005,6` in it). The edited value lives in `edit.raw` (period decimal, no
|
|
281
|
+
// grouping); every keystroke is cancelled in `beforeinput`, applied to raw by
|
|
282
|
+
// the engine (`numberDisplay.ts`) and written back through
|
|
283
|
+
// `execCommand('insertText')`, which keeps the browser's own undo. Anything
|
|
284
|
+
// the browser applies itself (IME, a non-cancelable Android keystroke, undo,
|
|
285
|
+
// autofill, a restore) is re-read from the text. `disableFormatting` without a
|
|
286
|
+
// `minPrecision` is today's code path, unchanged (`legacy`).
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Q60.25b: the currency or unit painted inside the text, as a shell around
|
|
290
|
+
* the number (`numberDisplay.ts`). `disableFormatting` ignores both; a code
|
|
291
|
+
* `Intl` refuses gives a plain number.
|
|
292
|
+
*/
|
|
293
|
+
const affix = computed(() =>
|
|
294
|
+
props.disableFormatting
|
|
295
|
+
? null
|
|
296
|
+
: affixOf(intlLocaleOf(config.locale), props.currency, props.unit)
|
|
297
|
+
);
|
|
298
|
+
/** The currency's own fraction digits (2 for EUR, 0 for JPY); `null` without one. */
|
|
299
|
+
const currencyDigits = computed(() =>
|
|
300
|
+
affix.value?.kind === 'currency'
|
|
301
|
+
? currencyDigitsOf(affix.value.code, affix.value.tag)
|
|
302
|
+
: null
|
|
303
|
+
);
|
|
304
|
+
/** An explicit prop wins; else the currency's digits; else 8 (and at least `minPrecision`). */
|
|
305
|
+
const maxPrecision = computed(
|
|
306
|
+
() =>
|
|
307
|
+
props.maxPrecision ??
|
|
308
|
+
(currencyDigits.value
|
|
309
|
+
? Math.max(currencyDigits.value.max, props.minPrecision ?? 0)
|
|
310
|
+
: 8)
|
|
311
|
+
);
|
|
312
|
+
const minPrecision = computed(
|
|
313
|
+
() => props.minPrecision ?? currencyDigits.value?.min ?? 0
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
/** Today's path: period display, Q51.2 on the whole text, `name` on the input. */
|
|
317
|
+
const legacy = computed(
|
|
318
|
+
() => !!props.disableFormatting && !(minPrecision.value > 0)
|
|
319
|
+
);
|
|
320
|
+
/** The locale's marks; period and no grouping when formatting is off. */
|
|
321
|
+
const symbols = computed(() =>
|
|
322
|
+
props.disableFormatting
|
|
323
|
+
? PLAIN_SYMBOLS
|
|
324
|
+
: numberSymbolsOf(
|
|
325
|
+
intlLocaleOf(config.locale),
|
|
326
|
+
affix.value?.kind === 'currency' ? affix.value.code : undefined
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
/** `maxPrecision` wins over a larger `minPrecision` (documented, no warning). */
|
|
330
|
+
const shownMinPrecision = computed(() =>
|
|
331
|
+
Math.max(0, Math.min(minPrecision.value, maxPrecision.value))
|
|
332
|
+
);
|
|
333
|
+
const editOptions = () => ({
|
|
334
|
+
maxPrecision: maxPrecision.value,
|
|
335
|
+
minPrecision: minPrecision.value,
|
|
336
|
+
max: props.max,
|
|
337
|
+
localeKey: symbols.value.decimal === ',' ? ',' : '.',
|
|
338
|
+
});
|
|
339
|
+
/** `raw` as the field shows it: the locale's number, in the affix's shell. */
|
|
340
|
+
const displayOf = (raw: string) =>
|
|
341
|
+
formatAffixed(
|
|
342
|
+
raw,
|
|
343
|
+
symbols.value,
|
|
344
|
+
{ minPrecision: shownMinPrecision.value },
|
|
345
|
+
affix.value
|
|
346
|
+
);
|
|
347
|
+
/** Text read back (paste, undo, restore) as raw: the shell stripped first. */
|
|
348
|
+
const readBack = (text: string) =>
|
|
349
|
+
readText(stripAffix(text, affix.value), symbols.value);
|
|
350
|
+
|
|
351
|
+
let edit: EditState = { raw: '', decChar: '', groupChar: '' };
|
|
352
|
+
let display: Display = { text: '', positions: [] };
|
|
353
|
+
/** The text this field last wrote; anything else in the input came from outside. */
|
|
354
|
+
let lastWritten = '';
|
|
355
|
+
/** Inside our own `execCommand` write: its nested events are ours. */
|
|
356
|
+
let busy = false;
|
|
357
|
+
/** The nested `input` of a write was emitted as the edit's `input`. */
|
|
358
|
+
let emittedInput = false;
|
|
359
|
+
let composing = false;
|
|
360
|
+
|
|
361
|
+
/** A model value as a number: a numeric string (an untyped app) counts too. */
|
|
362
|
+
const asNumber = (value: unknown): number | null => {
|
|
363
|
+
if (value === null || value === undefined || value === '') return null;
|
|
364
|
+
const number = Number(value);
|
|
365
|
+
return Number.isFinite(number) ? number : null;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The last valid value: what the model holds while the text is half-typed
|
|
370
|
+
* (`-`), what blur restores, and what the hidden input posts.
|
|
371
|
+
*/
|
|
372
|
+
const committed = ref<number | null>(asNumber(props.modelValue));
|
|
373
|
+
const submitValue = computed(() =>
|
|
374
|
+
rawAtRest(committed.value, maxPrecision.value)
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
/** The display of a value at rest: rebuilt from the number, useless digits gone. */
|
|
378
|
+
const restText = (value: number | null | undefined) =>
|
|
379
|
+
displayOf(rawAtRest(value, maxPrecision.value)).text;
|
|
380
|
+
|
|
381
|
+
/** Set once mounted: the server never prints `Intl` text (CURRENCY-SPEC § 2.11). */
|
|
382
|
+
const mounted = ref(false);
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Read only while not focused: a value text that changes per keystroke
|
|
386
|
+
* double-speaks. With a currency or unit, the spoken form (`1,234.50 euros`,
|
|
387
|
+
* `20 kilograms`), not the glyph. Only after mount, on every field: computed
|
|
388
|
+
* on the server, ICU drift between Node and the browser would be an attribute
|
|
389
|
+
* hydration mismatch.
|
|
390
|
+
*/
|
|
391
|
+
const ariaValuetext = computed(() => {
|
|
392
|
+
if (!mounted.value) return undefined;
|
|
393
|
+
if (props.disableFormatting || active.value || isNil(props.modelValue))
|
|
394
|
+
return undefined;
|
|
395
|
+
const value = asNumber(props.modelValue);
|
|
396
|
+
return affix.value && value !== null
|
|
397
|
+
? spokenAffixed(
|
|
398
|
+
rawAtRest(value, maxPrecision.value),
|
|
399
|
+
shownMinPrecision.value,
|
|
400
|
+
affix.value
|
|
401
|
+
)
|
|
402
|
+
: restText(value);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
/** A raw value from outside: the decimal counts as the locale's key (owner Q6). */
|
|
406
|
+
const setRaw = (raw: string) => {
|
|
407
|
+
edit = {
|
|
408
|
+
raw: withPaddedMark(raw, shownMinPrecision.value),
|
|
409
|
+
decChar: '',
|
|
410
|
+
groupChar: '',
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const isFocused = () =>
|
|
415
|
+
!!input.value && input.value.ownerDocument.activeElement === input.value;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Writes `displayOf(edit.raw)` into the input. A user edit goes through
|
|
419
|
+
* `execCommand('insertText')` over the changed span (a native undo step):
|
|
420
|
+
* the digits only while the shell stays (the symbol stays out of the undo
|
|
421
|
+
* step and the screen-reader echo), the whole value when the shell changes
|
|
422
|
+
* (first digit, sign, plural); anything else, or a failed command, sets
|
|
423
|
+
* `value`.
|
|
424
|
+
*/
|
|
425
|
+
const paint = (caretRaw?: number, asEdit = false) => {
|
|
426
|
+
const el = input.value;
|
|
427
|
+
if (!el) return;
|
|
428
|
+
const shell = display.shell;
|
|
429
|
+
display = displayOf(edit.raw);
|
|
430
|
+
const next = display.text;
|
|
431
|
+
if (el.value !== next) {
|
|
432
|
+
if (asEdit && isFocused()) writeAsEdit(el, next, shell !== display.shell);
|
|
433
|
+
if (el.value !== next) el.value = next;
|
|
434
|
+
}
|
|
435
|
+
lastWritten = next;
|
|
436
|
+
if (caretRaw !== undefined && isFocused()) {
|
|
437
|
+
const at = displayIndexOf(display, caretRaw);
|
|
438
|
+
el.setSelectionRange(at, at);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const writeAsEdit = (el: HTMLInputElement, next: string, whole: boolean) => {
|
|
443
|
+
const current = el.value;
|
|
444
|
+
let head = 0;
|
|
445
|
+
while (
|
|
446
|
+
!whole &&
|
|
447
|
+
head < current.length &&
|
|
448
|
+
head < next.length &&
|
|
449
|
+
current[head] === next[head]
|
|
450
|
+
)
|
|
451
|
+
head++;
|
|
452
|
+
let tail = 0;
|
|
453
|
+
if (writeStrategy.span === 'minimal' && !whole) {
|
|
454
|
+
while (
|
|
455
|
+
tail < current.length - head &&
|
|
456
|
+
tail < next.length - head &&
|
|
457
|
+
current[current.length - 1 - tail] === next[next.length - 1 - tail]
|
|
458
|
+
)
|
|
459
|
+
tail++;
|
|
460
|
+
}
|
|
461
|
+
const text = next.slice(head, next.length - tail);
|
|
462
|
+
busy = true;
|
|
463
|
+
try {
|
|
464
|
+
el.setSelectionRange(head, current.length - tail);
|
|
465
|
+
// A WebKit `execCommand` inside an input listener can re-enter it: the
|
|
466
|
+
// `busy` guard drops the nested events (WebKit hung without one).
|
|
467
|
+
if (text) document.execCommand('insertText', false, text);
|
|
468
|
+
else document.execCommand('delete');
|
|
469
|
+
} catch {
|
|
470
|
+
// Falls through to the `value` write in `paint`.
|
|
471
|
+
} finally {
|
|
472
|
+
busy = false;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/** Emits the typed value; a half-typed text (`-`) leaves the last valid one standing. */
|
|
477
|
+
const commitTyped = () => {
|
|
478
|
+
const value = valueOfRaw(edit.raw);
|
|
479
|
+
if (value === undefined) return;
|
|
480
|
+
committed.value = value;
|
|
481
|
+
emit('update:modelValue', value);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/** Applies one edit to raw and writes the result. */
|
|
485
|
+
const runEdit = (op: Edit, event: Event, asEdit: boolean) => {
|
|
486
|
+
const result = applyEdit(edit, op, editOptions());
|
|
487
|
+
if (result.rejected) {
|
|
488
|
+
// A refused key leaves the caret where it was when that is the same
|
|
489
|
+
// raw spot (Delete at `12,00|`: not pulled back before the placeholders).
|
|
490
|
+
const el = input.value;
|
|
491
|
+
const stays =
|
|
492
|
+
!!el &&
|
|
493
|
+
el.value === display.text &&
|
|
494
|
+
el.selectionStart === el.selectionEnd &&
|
|
495
|
+
rawIndexOf(display, el.selectionStart ?? 0) === result.caret;
|
|
496
|
+
edit = result;
|
|
497
|
+
paint(stays ? undefined : result.caret);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
edit = result;
|
|
501
|
+
emittedInput = false;
|
|
502
|
+
paint(result.caret, asEdit);
|
|
503
|
+
if (!emittedInput) emit('input', event);
|
|
504
|
+
commitTyped();
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/** The raw range of the input's selection. */
|
|
508
|
+
const rawSelection = (el: HTMLInputElement): [number, number] => [
|
|
509
|
+
rawIndexOf(display, el.selectionStart ?? el.value.length),
|
|
510
|
+
rawIndexOf(display, el.selectionEnd ?? el.value.length),
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
const DELETES: Record<string, Edit['type']> = {
|
|
514
|
+
deleteContentBackward: 'deleteBackward',
|
|
515
|
+
deleteContent: 'deleteBackward',
|
|
516
|
+
deleteByCut: 'deleteBackward',
|
|
517
|
+
deleteByDrag: 'deleteBackward',
|
|
518
|
+
deleteContentForward: 'deleteForward',
|
|
519
|
+
deleteWordBackward: 'deleteToStart',
|
|
520
|
+
deleteSoftLineBackward: 'deleteToStart',
|
|
521
|
+
deleteHardLineBackward: 'deleteToStart',
|
|
522
|
+
deleteWordForward: 'deleteToEnd',
|
|
523
|
+
deleteSoftLineForward: 'deleteToEnd',
|
|
524
|
+
deleteHardLineForward: 'deleteToEnd',
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* The text of an `insertReplacementText` (desktop spellcheck, a keyboard
|
|
529
|
+
* suggestion): `data`, or `dataTransfer` where Chrome and Firefox put it;
|
|
530
|
+
* `null` when the event carries neither.
|
|
531
|
+
*/
|
|
532
|
+
const replacementText = (event: InputEvent): string | null =>
|
|
533
|
+
event.data ??
|
|
534
|
+
(event.dataTransfer ? event.dataTransfer.getData('text/plain') : null);
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* A replacement over raw `[start, end)`: a word is text, read back like a
|
|
538
|
+
* paste (`$56,789.00` is 56789, not the keys `5` `6` `,` …); one character is
|
|
539
|
+
* a keystroke (`-` still toggles the sign); nothing deletes the range.
|
|
540
|
+
*/
|
|
541
|
+
const replacementEdit = (text: string, start: number, end: number): Edit =>
|
|
542
|
+
[...text].length > 1
|
|
543
|
+
? { type: 'paste', start, end, raw: readBack(text) }
|
|
544
|
+
: text
|
|
545
|
+
? { type: 'insert', start, end, text }
|
|
546
|
+
: { type: 'deleteBackward', start, end };
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Every edit the browser announces and lets us cancel: applied to raw. Undo
|
|
550
|
+
* and redo run natively (the steps are our own writes) and are re-read on
|
|
551
|
+
* `input`; IME composition and non-cancelable events take the fallback there.
|
|
552
|
+
*/
|
|
553
|
+
const onBeforeinput = (event: InputEvent) => {
|
|
554
|
+
const el = input.value;
|
|
555
|
+
if (legacy.value || busy || !el) return;
|
|
556
|
+
if (composing || event.isComposing || !event.cancelable) return;
|
|
557
|
+
// A keyboard that announces no text (Android, empty `data`): the browser
|
|
558
|
+
// applies it and the fallback reads it on `input` (GROK-INGEST 3).
|
|
559
|
+
if (event.inputType === 'insertText' && !event.data) return;
|
|
560
|
+
const type = event.inputType;
|
|
561
|
+
const [start, end] = rawSelection(el);
|
|
562
|
+
let op: Edit | undefined;
|
|
563
|
+
if (type === 'insertText') {
|
|
564
|
+
op = { type: 'insert', start, end, text: event.data ?? '' };
|
|
565
|
+
} else if (type === 'insertReplacementText') {
|
|
566
|
+
const text = replacementText(event);
|
|
567
|
+
// Nothing to read, or nothing over a caret: the browser applies it and
|
|
568
|
+
// the fallback reads it on `input`.
|
|
569
|
+
if (text === null || (!text && start === end)) return;
|
|
570
|
+
op = replacementEdit(text, start, end);
|
|
571
|
+
} else if (type === 'insertFromPaste' || type === 'insertFromDrop') {
|
|
572
|
+
const text = event.dataTransfer?.getData('text/plain') ?? event.data ?? '';
|
|
573
|
+
op = { type: 'paste', start, end, raw: readBack(text) };
|
|
574
|
+
} else if (DELETES[type]) {
|
|
575
|
+
const from = el.selectionStart ?? 0;
|
|
576
|
+
const to = el.selectionEnd ?? 0;
|
|
577
|
+
if (isShellOnly(display, from, to)) {
|
|
578
|
+
// `€` selected in `12,00 €`, then Backspace: nothing to delete. The
|
|
579
|
+
// caret collapses to the nearer edge of the number.
|
|
580
|
+
event.preventDefault();
|
|
581
|
+
const edge = snapOffset(
|
|
582
|
+
display,
|
|
583
|
+
to <= display.hole![0] ? display.hole![0] : display.hole![1]
|
|
584
|
+
);
|
|
585
|
+
el.setSelectionRange(edge, edge);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
op = { type: DELETES[type], start, end } as Edit;
|
|
589
|
+
}
|
|
590
|
+
if (!op) return;
|
|
591
|
+
event.preventDefault();
|
|
592
|
+
runEdit(op, event, true);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/** A paste reads the clipboard: a legal number in this locale strictly, else Q51.2. */
|
|
596
|
+
const onPasteFormatted = (event: ClipboardEvent) => {
|
|
597
|
+
const el = input.value;
|
|
598
|
+
if (legacy.value || !el || !event.clipboardData) return;
|
|
599
|
+
// Not cancelled: the browser's own paste into a readonly field does nothing.
|
|
600
|
+
if (fieldReadonly.value || props.disabled) return;
|
|
601
|
+
const [start, end] = rawSelection(el);
|
|
602
|
+
const text = event.clipboardData.getData('text/plain');
|
|
603
|
+
event.preventDefault();
|
|
604
|
+
runEdit({ type: 'paste', start, end, raw: readBack(text) }, event, true);
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/** A drag that started in this field: the browser's own move (`deleteByDrag` + `insertFromDrop`). */
|
|
608
|
+
let dragFromSelf = false;
|
|
609
|
+
|
|
610
|
+
/** The display offset under a drop point; the end when the engine cannot say. */
|
|
611
|
+
const dropOffsetOf = (el: HTMLInputElement, event: DragEvent) => {
|
|
612
|
+
const doc = el.ownerDocument as Document & {
|
|
613
|
+
caretPositionFromPoint?: (
|
|
614
|
+
x: number,
|
|
615
|
+
y: number
|
|
616
|
+
) => { offsetNode: Node; offset: number } | null;
|
|
617
|
+
};
|
|
618
|
+
const position = doc.caretPositionFromPoint?.(event.clientX, event.clientY);
|
|
619
|
+
return position?.offsetNode === el ? position.offset : el.value.length;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* A drop from outside is a paste at the drop point (PANEL § 2.6): cancelled
|
|
624
|
+
* and read here, never left to the default action (a synthetic drop has none,
|
|
625
|
+
* and its `insertFromDrop` would never come).
|
|
626
|
+
*/
|
|
627
|
+
const onDropFormatted = (event: DragEvent) => {
|
|
628
|
+
const el = input.value;
|
|
629
|
+
if (legacy.value || !el || !event.dataTransfer || dragFromSelf) return;
|
|
630
|
+
if (fieldReadonly.value || props.disabled) return;
|
|
631
|
+
const text = event.dataTransfer.getData('text/plain');
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
const at = rawIndexOf(display, dropOffsetOf(el, event));
|
|
634
|
+
el.focus();
|
|
635
|
+
runEdit(
|
|
636
|
+
{ type: 'paste', start: at, end: at, raw: readBack(text) },
|
|
637
|
+
event,
|
|
638
|
+
true
|
|
639
|
+
);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Re-reads text the browser put in the field — undo / redo, autofill, a
|
|
644
|
+
* restore, an outside `el.value` write — as a number in this locale.
|
|
645
|
+
*/
|
|
646
|
+
const rereadAll = () => {
|
|
647
|
+
const el = input.value;
|
|
648
|
+
if (!el) return;
|
|
649
|
+
const parsed = readBack(el.value);
|
|
650
|
+
const result = applyEdit(
|
|
651
|
+
{ raw: '', decChar: '', groupChar: '' },
|
|
652
|
+
{ type: 'paste', start: 0, end: 0, raw: parsed },
|
|
653
|
+
editOptions()
|
|
654
|
+
);
|
|
655
|
+
// Past the 20-character cap (owner round 7): refused, the field keeps
|
|
656
|
+
// its number and our text comes back.
|
|
657
|
+
if (result.rejected) {
|
|
658
|
+
paint(isFocused() ? edit.raw.length : undefined);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
setRaw(result.raw);
|
|
662
|
+
display = displayOf(edit.raw);
|
|
663
|
+
if (display.text === el.value) {
|
|
664
|
+
lastWritten = el.value;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
paint(isFocused() ? edit.raw.length : undefined);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The fallback for edits the browser applied itself (an Android keyboard's
|
|
672
|
+
* non-cancelable keystroke, a finished IME composition): the change between
|
|
673
|
+
* the text we last wrote and the text now there — its common prefix capped at
|
|
674
|
+
* the caret, so a deletion inside a run of equal digits lands where the user
|
|
675
|
+
* was — is mapped to raw and re-applied as a keystroke. Undo is lost on this
|
|
676
|
+
* path only.
|
|
677
|
+
*/
|
|
678
|
+
const applyBrowserEdit = (event: Event) => {
|
|
679
|
+
const el = input.value;
|
|
680
|
+
if (!el) return;
|
|
681
|
+
const before = lastWritten;
|
|
682
|
+
const after = el.value;
|
|
683
|
+
if (before === after) return;
|
|
684
|
+
if ((event as InputEvent).inputType === 'insertReplacementText') {
|
|
685
|
+
applyReplacement(event as InputEvent, before, after);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const caret = el.selectionStart ?? after.length;
|
|
689
|
+
// The change ends at the caret too: an insert inside a run of equal digits
|
|
690
|
+
// (`1.1|11` + `1`) is at the caret, not past it.
|
|
691
|
+
let tail = 0;
|
|
692
|
+
while (
|
|
693
|
+
tail < before.length &&
|
|
694
|
+
tail < after.length - caret &&
|
|
695
|
+
before[before.length - 1 - tail] === after[after.length - 1 - tail]
|
|
696
|
+
)
|
|
697
|
+
tail++;
|
|
698
|
+
let head = 0;
|
|
699
|
+
while (
|
|
700
|
+
head < before.length - tail &&
|
|
701
|
+
head < after.length - tail &&
|
|
702
|
+
head < caret &&
|
|
703
|
+
before[head] === after[head]
|
|
704
|
+
)
|
|
705
|
+
head++;
|
|
706
|
+
const inserted = after.slice(head, after.length - tail);
|
|
707
|
+
const start = rawIndexOf(display, head);
|
|
708
|
+
const end = rawIndexOf(display, before.length - tail);
|
|
709
|
+
// A shell character removed by the browser (an Android Backspace on `€`)
|
|
710
|
+
// heals: nothing to apply, the text is painted back.
|
|
711
|
+
if (!inserted && isShellOnly(display, head, before.length - tail)) {
|
|
712
|
+
const caret = rawIndexOf(display, head);
|
|
713
|
+
paint(caret);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// A deletion of a group mark alone (`start === end`) is a Backspace (or a
|
|
717
|
+
// Delete) next to it: the digit beyond goes.
|
|
718
|
+
const forward = /Forward$/.test((event as InputEvent).inputType ?? '');
|
|
719
|
+
const op: Edit = inserted
|
|
720
|
+
? { type: 'insert', start, end, text: inserted }
|
|
721
|
+
: { type: forward ? 'deleteForward' : 'deleteBackward', start, end };
|
|
722
|
+
runEdit(op, event, false);
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Where a replacement of `text` went: the `[from, to)` of `before` that
|
|
727
|
+
* `after` has `text` in place of, the one nearest the caret when the text
|
|
728
|
+
* could sit in several places (runs of equal digits). `null` when `after` is
|
|
729
|
+
* not `before` with one span swapped for `text`.
|
|
730
|
+
*/
|
|
731
|
+
const replacedSpan = (
|
|
732
|
+
before: string,
|
|
733
|
+
after: string,
|
|
734
|
+
text: string,
|
|
735
|
+
caret: number
|
|
736
|
+
): [number, number] | null => {
|
|
737
|
+
const removed = before.length - (after.length - text.length);
|
|
738
|
+
if (removed < 0) return null;
|
|
739
|
+
let best: number | null = null;
|
|
740
|
+
for (let at = 0; at + text.length <= after.length; at++) {
|
|
741
|
+
if (before.slice(0, at) !== after.slice(0, at)) break;
|
|
742
|
+
if (
|
|
743
|
+
after.slice(at, at + text.length) !== text ||
|
|
744
|
+
before.slice(at + removed) !== after.slice(at + text.length)
|
|
745
|
+
)
|
|
746
|
+
continue;
|
|
747
|
+
const distance = Math.abs(at + text.length - caret);
|
|
748
|
+
if (best === null || distance < Math.abs(best + text.length - caret))
|
|
749
|
+
best = at;
|
|
750
|
+
}
|
|
751
|
+
return best === null ? null : [best, best + removed];
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* A replacement the browser applied itself (non-cancelable: an Android
|
|
756
|
+
* suggestion or autocorrect). The text it carries is read back over the span
|
|
757
|
+
* it replaced, the same edit the cancelable path makes; never the diff
|
|
758
|
+
* fragment (`1.234` → `1.234.567` shares `1.234`, and `.567` alone is not
|
|
759
|
+
* the number). A replacement that says nothing of its text swaps the word,
|
|
760
|
+
* and a number field holds one: the whole new text is read back.
|
|
761
|
+
*/
|
|
762
|
+
const applyReplacement = (event: InputEvent, before: string, after: string) => {
|
|
763
|
+
const el = input.value!;
|
|
764
|
+
const text = replacementText(event);
|
|
765
|
+
const span =
|
|
766
|
+
text === null
|
|
767
|
+
? null
|
|
768
|
+
: replacedSpan(before, after, text, el.selectionStart ?? after.length);
|
|
769
|
+
const op: Edit =
|
|
770
|
+
text !== null && span
|
|
771
|
+
? replacementEdit(
|
|
772
|
+
text,
|
|
773
|
+
rawIndexOf(display, span[0]),
|
|
774
|
+
rawIndexOf(display, span[1])
|
|
775
|
+
)
|
|
776
|
+
: { type: 'paste', start: 0, end: edit.raw.length, raw: readBack(after) };
|
|
777
|
+
runEdit(op, event, false);
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const REREAD_TYPES = new Set(['historyUndo', 'historyRedo', '']);
|
|
781
|
+
|
|
782
|
+
const onFormattedInput = (event: Event) => {
|
|
783
|
+
if (busy) {
|
|
784
|
+
// The nested `input` of our own write: the edit's one `input` event.
|
|
785
|
+
emittedInput = true;
|
|
786
|
+
emit('input', event);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (composing) return;
|
|
790
|
+
const type = (event as InputEvent).inputType ?? '';
|
|
791
|
+
if (REREAD_TYPES.has(type) || !(event instanceof InputEvent)) {
|
|
792
|
+
rereadAll();
|
|
793
|
+
emit('input', event);
|
|
794
|
+
commitTyped();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
applyBrowserEdit(event);
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const onCompositionstart = () => {
|
|
801
|
+
if (!legacy.value) composing = true;
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
const onCompositionend = (event: CompositionEvent) => {
|
|
805
|
+
if (legacy.value || !composing) return;
|
|
806
|
+
composing = false;
|
|
807
|
+
applyBrowserEdit(event);
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
/** The hidden input a native POST reads (formatted mode with a `name`). */
|
|
811
|
+
const posted = ref<HTMLInputElement | null>(null);
|
|
812
|
+
|
|
813
|
+
/** The text differs from what we wrote (a restore, autofill): read it again. */
|
|
814
|
+
const reconcile = () => {
|
|
815
|
+
if (legacy.value || !input.value) return;
|
|
816
|
+
if (input.value.value !== lastWritten) {
|
|
817
|
+
rereadAll();
|
|
818
|
+
commitTyped();
|
|
819
|
+
// A submit reads the hidden input before Vue re-renders it.
|
|
820
|
+
if (posted.value) posted.value.value = submitValue.value;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
/** Blur shows the value at rest: rebuilt from the last valid value. */
|
|
825
|
+
const settle = (caretAtEnd = false) => {
|
|
826
|
+
if (legacy.value) return;
|
|
827
|
+
setRaw(rawAtRest(committed.value, maxPrecision.value));
|
|
828
|
+
paint(caretAtEnd ? edit.raw.length : undefined);
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const onPageshow = (event: PageTransitionEvent) => {
|
|
832
|
+
if (event.persisted) reconcile();
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* A silent autofill (no event) is read before the form submits: capture runs
|
|
837
|
+
* ahead of the consumer's own `submit` listener and of the form data. Heard
|
|
838
|
+
* on the document and matched against the form that owns the field at
|
|
839
|
+
* submit time, so a `form=` that changes after mount is followed.
|
|
840
|
+
*/
|
|
841
|
+
let submitDocument: Document | null = null;
|
|
842
|
+
const onSubmitCapture = (event: Event) => {
|
|
843
|
+
const owner = (posted.value ?? input.value)?.form;
|
|
844
|
+
if (owner && event.target === owner) reconcile();
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/** Between pointerdown and pointerup on the input: no clamp (a drag from the prefix). */
|
|
848
|
+
let pointerHeld = false;
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Q60.25b (CURRENCY-SPEC § 2.3): a collapsed caret never rests on a currency /
|
|
852
|
+
* unit shell. A click, an arrow, Home / End landing on it moves to the nearest
|
|
853
|
+
* edge of the number (never before a negative value's minus). Ranges are never
|
|
854
|
+
* touched: select-all and a double-click on `€` stay as they are (a copy keeps
|
|
855
|
+
* the symbol), and a delete over the shell alone does nothing (`onBeforeinput`).
|
|
856
|
+
* The clamp is cosmetic: an edit with the caret on the shell still edits the
|
|
857
|
+
* number's edge.
|
|
858
|
+
*/
|
|
859
|
+
const snapSelection = () => {
|
|
860
|
+
const el = input.value;
|
|
861
|
+
if (!el || !display.hole || busy || composing || pointerHeld) return;
|
|
862
|
+
if (!isFocused() || el.value !== display.text) return;
|
|
863
|
+
const at = el.selectionStart ?? 0;
|
|
864
|
+
if (at !== el.selectionEnd) return;
|
|
865
|
+
const snapped = snapOffset(display, at);
|
|
866
|
+
if (snapped !== at) el.setSelectionRange(snapped, snapped);
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const releasePointer = () => {
|
|
870
|
+
pointerHeld = false;
|
|
871
|
+
window.removeEventListener('pointerup', releasePointer);
|
|
872
|
+
window.removeEventListener('pointercancel', releasePointer);
|
|
873
|
+
snapSelection();
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const holdPointer = () => {
|
|
877
|
+
if (!display.hole) return;
|
|
878
|
+
pointerHeld = true;
|
|
879
|
+
window.addEventListener('pointerup', releasePointer);
|
|
880
|
+
window.addEventListener('pointercancel', releasePointer);
|
|
881
|
+
};
|
|
882
|
+
|
|
231
883
|
/**
|
|
232
884
|
* Applies precision and range guards to finalized numeric values. While the
|
|
233
885
|
* user types, only `max` applies (`withMin: false`): the digits of 15 pass
|
|
@@ -236,7 +888,7 @@ const input = ref<HTMLInputElement | null>(null);
|
|
|
236
888
|
*/
|
|
237
889
|
const normalizeNumericValue = (value: number, withMin = true): number => {
|
|
238
890
|
const precisionAligned = Number(
|
|
239
|
-
clampPrecision(value.toString(),
|
|
891
|
+
clampPrecision(value.toString(), maxPrecision.value)
|
|
240
892
|
);
|
|
241
893
|
return clamp(precisionAligned, withMin ? props.min : -Infinity, props.max);
|
|
242
894
|
};
|
|
@@ -247,6 +899,17 @@ const normalizeNumericValue = (value: number, withMin = true): number => {
|
|
|
247
899
|
* "15" with min 10 could not be typed through its "1".
|
|
248
900
|
*/
|
|
249
901
|
const applyMin = () => {
|
|
902
|
+
if (!legacy.value) {
|
|
903
|
+
// Enter re-reads an outside text first, as blur does (PANEL § 2.7).
|
|
904
|
+
reconcile();
|
|
905
|
+
const typed = valueOfRaw(edit.raw);
|
|
906
|
+
if (typeof typed !== 'number' || typed >= props.min) return;
|
|
907
|
+
setRaw(rawOfNumber(props.min, maxPrecision.value));
|
|
908
|
+
committed.value = props.min;
|
|
909
|
+
paint(isFocused() ? edit.raw.length : undefined);
|
|
910
|
+
emit('update:modelValue', props.min);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
250
913
|
if (!input.value?.value) return;
|
|
251
914
|
const typed = Number(makeValidNumberString(input.value.value));
|
|
252
915
|
if (!Number.isFinite(typed) || typed >= props.min) return;
|
|
@@ -295,6 +958,7 @@ const increase = () => {
|
|
|
295
958
|
* 7) Sync visible input and preserve caret position when transformed.
|
|
296
959
|
*/
|
|
297
960
|
const onInput = (event: Event) => {
|
|
961
|
+
if (!legacy.value) return onFormattedInput(event);
|
|
298
962
|
if (!input.value) return;
|
|
299
963
|
if (event.target instanceof HTMLInputElement) {
|
|
300
964
|
const value = event.target.value;
|
|
@@ -319,7 +983,7 @@ const onInput = (event: Event) => {
|
|
|
319
983
|
}
|
|
320
984
|
}
|
|
321
985
|
emit('update:modelValue', valueToEmit);
|
|
322
|
-
const clamped = clampPrecision(nextInputValue,
|
|
986
|
+
const clamped = clampPrecision(nextInputValue, maxPrecision.value);
|
|
323
987
|
if (input.value.value !== clamped) {
|
|
324
988
|
input.value.value = clamped;
|
|
325
989
|
if (selectionStart !== null && selectionEnd !== null) {
|
|
@@ -346,6 +1010,9 @@ const onKeydown = (event: KeyboardEvent) => {
|
|
|
346
1010
|
// Q56.14: before the browser's implicit submission (it follows keydown),
|
|
347
1011
|
// so a surrounding form submits the corrected value, not the typed one.
|
|
348
1012
|
applyMin();
|
|
1013
|
+
// Owner 2026-10-07: Enter shows the value at rest as blur does (`1,50`
|
|
1014
|
+
// → `1,5`), the caret after the number; not while an IME commits.
|
|
1015
|
+
if (!event.isComposing && !composing) settle(true);
|
|
349
1016
|
}
|
|
350
1017
|
};
|
|
351
1018
|
|
|
@@ -441,7 +1108,7 @@ const makeValidNumberString = (value: string): string => {
|
|
|
441
1108
|
let [first, second] = value.split('.');
|
|
442
1109
|
first = first || '0';
|
|
443
1110
|
if (!second) return first;
|
|
444
|
-
second = second?.slice(0,
|
|
1111
|
+
second = second?.slice(0, maxPrecision.value) || '0';
|
|
445
1112
|
const unified = `${first}.${second}`;
|
|
446
1113
|
return unified;
|
|
447
1114
|
};
|
|
@@ -452,18 +1119,27 @@ const makeValidNumberString = (value: string): string => {
|
|
|
452
1119
|
* bound separately (`@input.stop`).
|
|
453
1120
|
*/
|
|
454
1121
|
const eventListeners = {
|
|
1122
|
+
onBeforeinput: (event: InputEvent) => onBeforeinput(event),
|
|
455
1123
|
onBlur: (event: FocusEvent) => {
|
|
456
1124
|
active.value = false;
|
|
1125
|
+
reconcile();
|
|
457
1126
|
applyMin();
|
|
1127
|
+
settle();
|
|
458
1128
|
emit('blur', event);
|
|
459
1129
|
},
|
|
460
1130
|
onChange: (event: Event) => emit('change', event),
|
|
461
1131
|
onClick: (event: MouseEvent) => emit('click', event),
|
|
462
|
-
onCompositionend: (event: CompositionEvent) =>
|
|
463
|
-
|
|
464
|
-
emit('
|
|
1132
|
+
onCompositionend: (event: CompositionEvent) => {
|
|
1133
|
+
onCompositionend(event);
|
|
1134
|
+
emit('compositionend', event);
|
|
1135
|
+
},
|
|
1136
|
+
onCompositionstart: (event: CompositionEvent) => {
|
|
1137
|
+
onCompositionstart();
|
|
1138
|
+
emit('compositionstart', event);
|
|
1139
|
+
},
|
|
465
1140
|
onFocus: (event: FocusEvent) => {
|
|
466
1141
|
active.value = true;
|
|
1142
|
+
reconcile();
|
|
467
1143
|
emit('focus', event);
|
|
468
1144
|
},
|
|
469
1145
|
onKeydown: (event: KeyboardEvent) => {
|
|
@@ -473,7 +1149,16 @@ const eventListeners = {
|
|
|
473
1149
|
onKeyup: (event: KeyboardEvent) => emit('keyup', event),
|
|
474
1150
|
onMousedown: (event: MouseEvent) => emit('mousedown', event),
|
|
475
1151
|
onMouseup: (event: MouseEvent) => emit('mouseup', event),
|
|
476
|
-
|
|
1152
|
+
onPointerdown: () => holdPointer(),
|
|
1153
|
+
onPaste: (event: ClipboardEvent) => {
|
|
1154
|
+
onPasteFormatted(event);
|
|
1155
|
+
emit('paste', event);
|
|
1156
|
+
},
|
|
1157
|
+
onDragstart: () => (dragFromSelf = true),
|
|
1158
|
+
onDragend: () => (dragFromSelf = false),
|
|
1159
|
+
onDrop: (event: DragEvent) => onDropFormatted(event),
|
|
1160
|
+
// Fires on the control for click, keyboard, touch and script moves.
|
|
1161
|
+
onSelectionchange: () => snapSelection(),
|
|
477
1162
|
};
|
|
478
1163
|
|
|
479
1164
|
/**
|
|
@@ -496,6 +1181,15 @@ const inputBindings = computed(() =>
|
|
|
496
1181
|
*/
|
|
497
1182
|
const alignInputValue = (value: string | number | null) => {
|
|
498
1183
|
if (isNil(input.value)) return;
|
|
1184
|
+
if (!legacy.value) {
|
|
1185
|
+
committed.value = asNumber(value);
|
|
1186
|
+
// The echo of what the text emits, while the user types: the text stays
|
|
1187
|
+
// as typed (`10,` holds 10). Anything else replaces it (Q57.4).
|
|
1188
|
+
if (isFocused() && valueOfRaw(edit.raw) === committed.value) return;
|
|
1189
|
+
setRaw(rawAtRest(committed.value, maxPrecision.value));
|
|
1190
|
+
paint(isFocused() ? edit.raw.length : undefined);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
499
1193
|
|
|
500
1194
|
const currentValue = input.value.value;
|
|
501
1195
|
// This is triggered while the user is writing numbers like -0.75. Only while
|
|
@@ -536,13 +1230,51 @@ watch(
|
|
|
536
1230
|
* the `value` attribute (`v-ssr-value`, server-only) — the same string the
|
|
537
1231
|
* mount-time `alignInputValue` writes, so nothing changes at hydration.
|
|
538
1232
|
*/
|
|
539
|
-
const ssrDisplayValue = () =>
|
|
1233
|
+
const ssrDisplayValue = () =>
|
|
1234
|
+
legacy.value
|
|
1235
|
+
? props.modelValue?.toString() || ''
|
|
1236
|
+
: rawAtRest(asNumber(props.modelValue), maxPrecision.value);
|
|
540
1237
|
|
|
541
1238
|
/**
|
|
542
1239
|
* Ensures initial value is painted once the native input ref exists.
|
|
543
1240
|
*/
|
|
544
1241
|
onMounted(() => {
|
|
1242
|
+
// Focused before hydration (PANEL § 2.11): the server text is raw, so its
|
|
1243
|
+
// caret is a raw index; the formatted paint keeps it on the same digit.
|
|
1244
|
+
const el = input.value;
|
|
1245
|
+
// No `focus` event will come for a focus that predates mount.
|
|
1246
|
+
if (isFocused()) active.value = true;
|
|
1247
|
+
const caretRaw =
|
|
1248
|
+
el && !legacy.value && isFocused() && el.value === ssrDisplayValue()
|
|
1249
|
+
? el.selectionStart
|
|
1250
|
+
: null;
|
|
545
1251
|
alignInputValue(props.modelValue);
|
|
1252
|
+
if (el && caretRaw !== null && isFocused()) {
|
|
1253
|
+
const at = displayIndexOf(display, Math.min(caretRaw, edit.raw.length));
|
|
1254
|
+
el.setSelectionRange(at, at);
|
|
1255
|
+
}
|
|
1256
|
+
window.addEventListener('pageshow', onPageshow);
|
|
1257
|
+
submitDocument = el?.ownerDocument ?? null;
|
|
1258
|
+
submitDocument?.addEventListener('submit', onSubmitCapture, true);
|
|
1259
|
+
mounted.value = true;
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
onBeforeUnmount(() => {
|
|
1263
|
+
window.removeEventListener('pageshow', onPageshow);
|
|
1264
|
+
submitDocument?.removeEventListener('submit', onSubmitCapture, true);
|
|
1265
|
+
window.removeEventListener('pointerup', releasePointer);
|
|
1266
|
+
window.removeEventListener('pointercancel', releasePointer);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// A new locale or precision repaints the same raw value.
|
|
1270
|
+
watch([symbols, shownMinPrecision, maxPrecision, legacy, affix], () => {
|
|
1271
|
+
if (!input.value) return;
|
|
1272
|
+
if (legacy.value) {
|
|
1273
|
+
input.value.value = props.modelValue?.toString() || '';
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (!isFocused()) setRaw(rawAtRest(committed.value, maxPrecision.value));
|
|
1277
|
+
paint(isFocused() ? edit.raw.length : undefined);
|
|
546
1278
|
});
|
|
547
1279
|
</script>
|
|
548
1280
|
|
|
@@ -589,6 +1321,66 @@ export const BB_NUMBER_INPUT_PT = {
|
|
|
589
1321
|
states: readonly BbNumberInputPtState[];
|
|
590
1322
|
};
|
|
591
1323
|
|
|
1324
|
+
/**
|
|
1325
|
+
* The 45 simple units `Intl.NumberFormat` sanctions (ECMA-402;
|
|
1326
|
+
* `Intl.supportedValuesOf('unit')`).
|
|
1327
|
+
*/
|
|
1328
|
+
export type BbNumberSimpleUnit =
|
|
1329
|
+
| 'acre'
|
|
1330
|
+
| 'bit'
|
|
1331
|
+
| 'byte'
|
|
1332
|
+
| 'celsius'
|
|
1333
|
+
| 'centimeter'
|
|
1334
|
+
| 'day'
|
|
1335
|
+
| 'degree'
|
|
1336
|
+
| 'fahrenheit'
|
|
1337
|
+
| 'fluid-ounce'
|
|
1338
|
+
| 'foot'
|
|
1339
|
+
| 'gallon'
|
|
1340
|
+
| 'gigabit'
|
|
1341
|
+
| 'gigabyte'
|
|
1342
|
+
| 'gram'
|
|
1343
|
+
| 'hectare'
|
|
1344
|
+
| 'hour'
|
|
1345
|
+
| 'inch'
|
|
1346
|
+
| 'kilobit'
|
|
1347
|
+
| 'kilobyte'
|
|
1348
|
+
| 'kilogram'
|
|
1349
|
+
| 'kilometer'
|
|
1350
|
+
| 'liter'
|
|
1351
|
+
| 'megabit'
|
|
1352
|
+
| 'megabyte'
|
|
1353
|
+
| 'meter'
|
|
1354
|
+
| 'microsecond'
|
|
1355
|
+
| 'mile'
|
|
1356
|
+
| 'mile-scandinavian'
|
|
1357
|
+
| 'milliliter'
|
|
1358
|
+
| 'millimeter'
|
|
1359
|
+
| 'millisecond'
|
|
1360
|
+
| 'minute'
|
|
1361
|
+
| 'month'
|
|
1362
|
+
| 'nanosecond'
|
|
1363
|
+
| 'ounce'
|
|
1364
|
+
| 'percent'
|
|
1365
|
+
| 'petabyte'
|
|
1366
|
+
| 'pound'
|
|
1367
|
+
| 'second'
|
|
1368
|
+
| 'stone'
|
|
1369
|
+
| 'terabit'
|
|
1370
|
+
| 'terabyte'
|
|
1371
|
+
| 'week'
|
|
1372
|
+
| 'yard'
|
|
1373
|
+
| 'year';
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* A unit for `BbNumberInput`'s `unit`: a sanctioned simple unit, or two joined
|
|
1377
|
+
* by `-per-` (`kilometer-per-hour`; the halves are not checked, so the
|
|
1378
|
+
* manifest stays small). `square-meter` and `kilowatt` are not
|
|
1379
|
+
* `Intl` units, so they are type errors. `percent` holds the number you see:
|
|
1380
|
+
* 15 shows `15%`.
|
|
1381
|
+
*/
|
|
1382
|
+
export type BbNumberUnit = BbNumberSimpleUnit | `${string}-per-${string}`;
|
|
1383
|
+
|
|
592
1384
|
// The passthrough props are a mapped type written INLINE with this
|
|
593
1385
|
// component's own unions (Vue's SFC type resolver does not substitute a
|
|
594
1386
|
// generic alias's type parameters — plans/PASSTHROUGH.md T20); the test
|
|
@@ -635,6 +1427,16 @@ export type BbNumberInputProps = {
|
|
|
635
1427
|
*/
|
|
636
1428
|
compact?: boolean;
|
|
637
1429
|
|
|
1430
|
+
/**
|
|
1431
|
+
* An ISO 4217 code (`'EUR'`, `'USD'`, `'JPY'`): the field shows the amount
|
|
1432
|
+
* the locale's way, symbol included (`1.234,56 €` in Italian, `$1,234.56`
|
|
1433
|
+
* in English), inside the text, so `#prefix` / `#suffix` stay free. The
|
|
1434
|
+
* model is the number (`1234.56`). The decimals default to the currency's
|
|
1435
|
+
* own; `minPrecision` / `maxPrecision` win when set. One or the other with
|
|
1436
|
+
* `unit`.
|
|
1437
|
+
*/
|
|
1438
|
+
currency?: string;
|
|
1439
|
+
|
|
638
1440
|
/**
|
|
639
1441
|
* Visual variant of the field box — the same names and tokens as the
|
|
640
1442
|
* `BbButton` variants. Colours only: height, padding and border width are
|
|
@@ -761,12 +1563,37 @@ export type BbNumberInputProps = {
|
|
|
761
1563
|
max?: number;
|
|
762
1564
|
|
|
763
1565
|
/**
|
|
764
|
-
* Maximum precision of the number
|
|
1566
|
+
* Maximum precision of the number: fraction digits past it are refused as
|
|
1567
|
+
* typed (never rounded). With `currency`, the currency's own digits (`2`
|
|
1568
|
+
* for EUR, `0` for JPY, `3` for KWD), or `minPrecision` when that is larger.
|
|
765
1569
|
*
|
|
766
1570
|
* @defaultValue `8`
|
|
767
1571
|
*/
|
|
768
1572
|
maxPrecision?: number;
|
|
769
1573
|
|
|
1574
|
+
/**
|
|
1575
|
+
* Fraction digits always shown (`2`: `1` shows `1,00`, `1,5` shows `1,50`),
|
|
1576
|
+
* also while typing, where the padding zeros are placeholders the user types
|
|
1577
|
+
* over. The model stays the number (`1.5`). A `minPrecision` above
|
|
1578
|
+
* `maxPrecision` gives way to it. Applies with `disableFormatting` too
|
|
1579
|
+
* (the padding only, period decimal). With `currency`, the currency's own
|
|
1580
|
+
* digits (`2` for EUR, `0` for JPY).
|
|
1581
|
+
*
|
|
1582
|
+
* @defaultValue `0`
|
|
1583
|
+
*/
|
|
1584
|
+
minPrecision?: number;
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* Shows the number as typed, with a period decimal and no grouping — the
|
|
1588
|
+
* field before Q60.25: no locale display, `name` on the visible input.
|
|
1589
|
+
* Without it the field shows the number the locale's way while the user
|
|
1590
|
+
* types (`10.005,6` in Italian), groups it for them, and posts the raw value
|
|
1591
|
+
* (`10005.6`) through a hidden input carrying `name`.
|
|
1592
|
+
*
|
|
1593
|
+
* @defaultValue `false`
|
|
1594
|
+
*/
|
|
1595
|
+
disableFormatting?: boolean;
|
|
1596
|
+
|
|
770
1597
|
/**
|
|
771
1598
|
* Minimum value cap. Applied on blur and on Enter (before a surrounding
|
|
772
1599
|
* form submits), not while typing, so a value above a `min` greater than 1
|
|
@@ -826,6 +1653,15 @@ export type BbNumberInputProps = {
|
|
|
826
1653
|
* @defaultValue `1`
|
|
827
1654
|
*/
|
|
828
1655
|
step?: number;
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* A unit `Intl.NumberFormat` knows (`'kilogram'`, `'kilometer-per-hour'`,
|
|
1659
|
+
* `'celsius'`, `'percent'`; a typo is a type error): the field shows it
|
|
1660
|
+
* the locale's way inside the text (`20 kg`, `15 %`). The model is the number you see: `unit="percent"`
|
|
1661
|
+
* showing `15 %` holds `15`, not `0.15`. One or the other with `currency`
|
|
1662
|
+
* (set both and `currency` wins).
|
|
1663
|
+
*/
|
|
1664
|
+
unit?: BbNumberUnit;
|
|
829
1665
|
} & FormFieldProps<number | string | null>;
|
|
830
1666
|
|
|
831
1667
|
export type BbNumberInputEvents = {
|