@zerodep-dev/vue-tsx-pro-ui 0.2.0 → 0.3.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/dist/base/Badge.js +3 -3
- package/dist/base/Badge.js.map +1 -1
- package/dist/base/Can.d.ts +12 -0
- package/dist/base/Can.js +20 -0
- package/dist/base/Can.js.map +1 -0
- package/dist/base/ClientOnly.d.ts +10 -0
- package/dist/base/ClientOnly.js +18 -0
- package/dist/base/ClientOnly.js.map +1 -0
- package/dist/base/CloseButton.js +3 -3
- package/dist/base/CloseButton.js.map +1 -1
- package/dist/base/CodeBlock.js +5 -5
- package/dist/base/CodeBlock.js.map +1 -1
- package/dist/base/CopyButton.js +3 -3
- package/dist/base/CopyButton.js.map +1 -1
- package/dist/base/Icon.js +3 -3
- package/dist/base/Icon.js.map +1 -1
- package/dist/base/Indicator.js +3 -3
- package/dist/base/Indicator.js.map +1 -1
- package/dist/base/StatusDot.js +3 -3
- package/dist/base/StatusDot.js.map +1 -1
- package/dist/base/Tag.js +5 -5
- package/dist/base/Tag.js.map +1 -1
- package/dist/base/button-core.js +4 -4
- package/dist/base/button-core.js.map +1 -1
- package/dist/charts/AreaChart.js +3 -3
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.js +3 -3
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/ChordChart.js +3 -3
- package/dist/charts/ChordChart.js.map +1 -1
- package/dist/charts/ComboChart.js +3 -3
- package/dist/charts/ComboChart.js.map +1 -1
- package/dist/charts/DonutChart.js +3 -3
- package/dist/charts/DonutChart.js.map +1 -1
- package/dist/charts/FunnelChart.js +5 -5
- package/dist/charts/FunnelChart.js.map +1 -1
- package/dist/charts/GraphChart.js +3 -3
- package/dist/charts/GraphChart.js.map +1 -1
- package/dist/charts/LineChart.js +3 -3
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/MapChart.js +7 -7
- package/dist/charts/MapChart.js.map +1 -1
- package/dist/charts/RadarChart.js +16 -15
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/SankeyChart.js +3 -3
- package/dist/charts/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart.js +3 -3
- package/dist/charts/ScatterChart.js.map +1 -1
- package/dist/charts/TimelineChart.js +6 -5
- package/dist/charts/TimelineChart.js.map +1 -1
- package/dist/charts/TreemapChart.js +3 -3
- package/dist/charts/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart.js +3 -3
- package/dist/charts/WaterfallChart.js.map +1 -1
- package/dist/charts/XYChart.js +9 -7
- package/dist/charts/XYChart.js.map +1 -1
- package/dist/charts/frame.js +10 -8
- package/dist/charts/frame.js.map +1 -1
- package/dist/charts/single.js +4 -3
- package/dist/charts/single.js.map +1 -1
- package/dist/config/Provider.d.ts +3 -1
- package/dist/config/Provider.js +9 -4
- package/dist/config/Provider.js.map +1 -1
- package/dist/config/context.d.ts +18 -0
- package/dist/config/context.js +16 -1
- package/dist/config/context.js.map +1 -1
- package/dist/config/createUi.d.ts +20 -3
- package/dist/config/createUi.js +80 -49
- package/dist/config/createUi.js.map +1 -1
- package/dist/config/layout/preference.d.ts +43 -0
- package/dist/config/layout/preference.js +108 -0
- package/dist/config/layout/preference.js.map +1 -0
- package/dist/date/Calendar.js +3 -3
- package/dist/date/Calendar.js.map +1 -1
- package/dist/date/CalendarBoard.js +9 -9
- package/dist/date/CalendarBoard.js.map +1 -1
- package/dist/date/CalendarView.js +7 -7
- package/dist/date/CalendarView.js.map +1 -1
- package/dist/date/DateField.js +3 -3
- package/dist/date/DateField.js.map +1 -1
- package/dist/date/DatePicker.js +5 -5
- package/dist/date/DatePicker.js.map +1 -1
- package/dist/date/DateRangePicker.js +5 -5
- package/dist/date/DateRangePicker.js.map +1 -1
- package/dist/date/DateSegmentGroup.js +5 -5
- package/dist/date/DateSegmentGroup.js.map +1 -1
- package/dist/date/QuarterCalendar.js +6 -6
- package/dist/date/QuarterCalendar.js.map +1 -1
- package/dist/date/RangeCalendar.js +3 -3
- package/dist/date/RangeCalendar.js.map +1 -1
- package/dist/date/SegmentedField.js +3 -3
- package/dist/date/SegmentedField.js.map +1 -1
- package/dist/date/TimeField.js +3 -3
- package/dist/date/TimeField.js.map +1 -1
- package/dist/date/TimePicker.js +4 -4
- package/dist/date/TimePicker.js.map +1 -1
- package/dist/date/date-segments.js +1 -1
- package/dist/date/date-segments.js.map +1 -1
- package/dist/date/picker-popover.js +3 -2
- package/dist/date/picker-popover.js.map +1 -1
- package/dist/diagram/Diagram.js +12 -12
- package/dist/diagram/Diagram.js.map +1 -1
- package/dist/diagram/DiagramLanes.js +3 -3
- package/dist/diagram/DiagramLanes.js.map +1 -1
- package/dist/diagram/DiagramMinimap.js +4 -4
- package/dist/diagram/DiagramMinimap.js.map +1 -1
- package/dist/display/Accordion.js +6 -6
- package/dist/display/Accordion.js.map +1 -1
- package/dist/display/AnimatedNumber.js +4 -4
- package/dist/display/AnimatedNumber.js.map +1 -1
- package/dist/display/Avatar.js +4 -4
- package/dist/display/Avatar.js.map +1 -1
- package/dist/display/Checklist.js +7 -7
- package/dist/display/Checklist.js.map +1 -1
- package/dist/display/Collapse.js +8 -5
- package/dist/display/Collapse.js.map +1 -1
- package/dist/display/Collapsible.js +3 -3
- package/dist/display/Collapsible.js.map +1 -1
- package/dist/display/Descriptions.js +5 -5
- package/dist/display/Descriptions.js.map +1 -1
- package/dist/display/Ellipsis.js +3 -3
- package/dist/display/Ellipsis.js.map +1 -1
- package/dist/display/FieldCompare.js +4 -4
- package/dist/display/FieldCompare.js.map +1 -1
- package/dist/display/Gauge.js +11 -11
- package/dist/display/Gauge.js.map +1 -1
- package/dist/display/Image.js +4 -4
- package/dist/display/Image.js.map +1 -1
- package/dist/display/Marquee.js +3 -3
- package/dist/display/Marquee.js.map +1 -1
- package/dist/display/ProportionBar.js +4 -4
- package/dist/display/ProportionBar.js.map +1 -1
- package/dist/display/Sortable.js +4 -4
- package/dist/display/Sortable.js.map +1 -1
- package/dist/display/Sparkline.js +6 -6
- package/dist/display/Sparkline.js.map +1 -1
- package/dist/display/Stat.js +3 -3
- package/dist/display/Stat.js.map +1 -1
- package/dist/display/Timeline.js +7 -7
- package/dist/display/Timeline.js.map +1 -1
- package/dist/display/Tree.js +10 -10
- package/dist/display/Tree.js.map +1 -1
- package/dist/display/VirtualList.js +7 -7
- package/dist/display/VirtualList.js.map +1 -1
- package/dist/display/Watermark.js +3 -3
- package/dist/display/Watermark.js.map +1 -1
- package/dist/docs-kit/CodeGroup.js +5 -5
- package/dist/docs-kit/CodeGroup.js.map +1 -1
- package/dist/docs-kit/CodePreview.js +6 -6
- package/dist/docs-kit/CodePreview.js.map +1 -1
- package/dist/docs-kit/PropsTable.js +4 -4
- package/dist/docs-kit/PropsTable.js.map +1 -1
- package/dist/feedback/Alert.js +5 -5
- package/dist/feedback/Alert.js.map +1 -1
- package/dist/feedback/Banner.js +5 -5
- package/dist/feedback/Banner.js.map +1 -1
- package/dist/feedback/Empty.js +4 -4
- package/dist/feedback/Empty.js.map +1 -1
- package/dist/feedback/LoadingBar.js +20 -13
- package/dist/feedback/LoadingBar.js.map +1 -1
- package/dist/feedback/LoadingOverlay.js +3 -3
- package/dist/feedback/LoadingOverlay.js.map +1 -1
- package/dist/feedback/Progress.js +14 -14
- package/dist/feedback/Progress.js.map +1 -1
- package/dist/feedback/Result.js +4 -4
- package/dist/feedback/Result.js.map +1 -1
- package/dist/feedback/SkeletonText.js +3 -3
- package/dist/feedback/SkeletonText.js.map +1 -1
- package/dist/feedback/Toast.js +6 -6
- package/dist/feedback/Toast.js.map +1 -1
- package/dist/form/Field.js +7 -7
- package/dist/form/Field.js.map +1 -1
- package/dist/form/Form.js +3 -3
- package/dist/form/Form.js.map +1 -1
- package/dist/form/FormErrorSummary.js +3 -3
- package/dist/form/FormErrorSummary.js.map +1 -1
- package/dist/form/FormSection.js +6 -6
- package/dist/form/FormSection.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +10 -5
- package/dist/input/Autocomplete.js +3 -3
- package/dist/input/Autocomplete.js.map +1 -1
- package/dist/input/Cascader.js +7 -7
- package/dist/input/Cascader.js.map +1 -1
- package/dist/input/CharacterCount.js +3 -3
- package/dist/input/CharacterCount.js.map +1 -1
- package/dist/input/Checkbox.js +5 -5
- package/dist/input/Checkbox.js.map +1 -1
- package/dist/input/ColorField.js +4 -4
- package/dist/input/ColorField.js.map +1 -1
- package/dist/input/Combobox.js +4 -4
- package/dist/input/Combobox.js.map +1 -1
- package/dist/input/ConditionBuilder.js +15 -14
- package/dist/input/ConditionBuilder.js.map +1 -1
- package/dist/input/FileDrop.js +3 -3
- package/dist/input/FileDrop.js.map +1 -1
- package/dist/input/Listbox.js +9 -9
- package/dist/input/Listbox.js.map +1 -1
- package/dist/input/Mentions.js +3 -3
- package/dist/input/Mentions.js.map +1 -1
- package/dist/input/NumberField.js +3 -3
- package/dist/input/NumberField.js.map +1 -1
- package/dist/input/NumberRangeField.js +5 -5
- package/dist/input/NumberRangeField.js.map +1 -1
- package/dist/input/OtpField.js +5 -5
- package/dist/input/OtpField.js.map +1 -1
- package/dist/input/PeoplePicker.js +6 -6
- package/dist/input/PeoplePicker.js.map +1 -1
- package/dist/input/PeoplePickerPanel.js +4 -4
- package/dist/input/PeoplePickerPanel.js.map +1 -1
- package/dist/input/Radio.js +4 -4
- package/dist/input/Radio.js.map +1 -1
- package/dist/input/Rating.js +5 -5
- package/dist/input/Rating.js.map +1 -1
- package/dist/input/Select.js +4 -4
- package/dist/input/Select.js.map +1 -1
- package/dist/input/SelectedTags.js +4 -4
- package/dist/input/SelectedTags.js.map +1 -1
- package/dist/input/Slider.js +6 -6
- package/dist/input/Slider.js.map +1 -1
- package/dist/input/Switch.js +3 -3
- package/dist/input/Switch.js.map +1 -1
- package/dist/input/TagsInput.js +5 -5
- package/dist/input/TagsInput.js.map +1 -1
- package/dist/input/TextField.js +5 -5
- package/dist/input/TextField.js.map +1 -1
- package/dist/input/Textarea.js +4 -4
- package/dist/input/Textarea.js.map +1 -1
- package/dist/input/ToggleGroup.js +4 -4
- package/dist/input/ToggleGroup.js.map +1 -1
- package/dist/input/Transfer.js +8 -8
- package/dist/input/Transfer.js.map +1 -1
- package/dist/input/TreeSelect.js +6 -6
- package/dist/input/TreeSelect.js.map +1 -1
- package/dist/input/Upload.js +8 -8
- package/dist/input/Upload.js.map +1 -1
- package/dist/layout/Affix.js +4 -4
- package/dist/layout/Affix.js.map +1 -1
- package/dist/layout/ResizeHandle.d.ts +6 -2
- package/dist/layout/ResizeHandle.js +32 -9
- package/dist/layout/ResizeHandle.js.map +1 -1
- package/dist/layout/ScrollArea.js +3 -3
- package/dist/layout/ScrollArea.js.map +1 -1
- package/dist/layout/Separator.js +3 -3
- package/dist/layout/Separator.js.map +1 -1
- package/dist/layout/Splitter.js +3 -3
- package/dist/layout/Splitter.js.map +1 -1
- package/dist/layout/ViewportFill.d.ts +2 -2
- package/dist/layout/ViewportFill.js +4 -4
- package/dist/layout/ViewportFill.js.map +1 -1
- package/dist/navigation/Breadcrumbs.js +4 -4
- package/dist/navigation/Breadcrumbs.js.map +1 -1
- package/dist/navigation/NavBar.js +6 -6
- package/dist/navigation/NavBar.js.map +1 -1
- package/dist/navigation/NavList.d.ts +1 -0
- package/dist/navigation/NavList.js +20 -12
- package/dist/navigation/NavList.js.map +1 -1
- package/dist/navigation/NavMenu.d.ts +27 -0
- package/dist/navigation/NavMenu.js +54 -0
- package/dist/navigation/NavMenu.js.map +1 -0
- package/dist/navigation/PageJump.js +3 -3
- package/dist/navigation/PageJump.js.map +1 -1
- package/dist/navigation/PageSizeSelect.js +4 -4
- package/dist/navigation/PageSizeSelect.js.map +1 -1
- package/dist/navigation/Pagination.js +6 -6
- package/dist/navigation/Pagination.js.map +1 -1
- package/dist/navigation/PaginationBar.js +5 -5
- package/dist/navigation/PaginationBar.js.map +1 -1
- package/dist/navigation/Steps.js +4 -4
- package/dist/navigation/Steps.js.map +1 -1
- package/dist/navigation/TableOfContents.js +5 -5
- package/dist/navigation/TableOfContents.js.map +1 -1
- package/dist/navigation/Tabs.js +7 -7
- package/dist/navigation/Tabs.js.map +1 -1
- package/dist/navigation/Toolbar.js +3 -3
- package/dist/navigation/Toolbar.js.map +1 -1
- package/dist/overlay/AlertDialog.js +4 -4
- package/dist/overlay/AlertDialog.js.map +1 -1
- package/dist/overlay/CommandPalette.js +4 -4
- package/dist/overlay/CommandPalette.js.map +1 -1
- package/dist/overlay/ContextMenu.js +3 -3
- package/dist/overlay/ContextMenu.js.map +1 -1
- package/dist/overlay/Dialog.js +4 -4
- package/dist/overlay/Dialog.js.map +1 -1
- package/dist/overlay/Drawer.js +5 -5
- package/dist/overlay/Drawer.js.map +1 -1
- package/dist/overlay/HoverCard.js +5 -3
- package/dist/overlay/HoverCard.js.map +1 -1
- package/dist/overlay/Lightbox.js +5 -5
- package/dist/overlay/Lightbox.js.map +1 -1
- package/dist/overlay/Menu.js +7 -7
- package/dist/overlay/Menu.js.map +1 -1
- package/dist/overlay/ModalCore.js +9 -9
- package/dist/overlay/ModalCore.js.map +1 -1
- package/dist/overlay/Popconfirm.js +6 -6
- package/dist/overlay/Popconfirm.js.map +1 -1
- package/dist/overlay/Popover.js +5 -3
- package/dist/overlay/Popover.js.map +1 -1
- package/dist/overlay/Tooltip.js +5 -3
- package/dist/overlay/Tooltip.js.map +1 -1
- package/dist/overlay/Tour.js +8 -8
- package/dist/overlay/Tour.js.map +1 -1
- package/dist/overlay/detached.d.ts +2 -0
- package/dist/overlay/detached.js +73 -42
- package/dist/overlay/detached.js.map +1 -1
- package/dist/patterns/ActionBar.js +3 -3
- package/dist/patterns/ActionBar.js.map +1 -1
- package/dist/patterns/AppShell.d.ts +28 -1
- package/dist/patterns/AppShell.js +158 -52
- package/dist/patterns/AppShell.js.map +1 -1
- package/dist/patterns/AuthLayout.d.ts +28 -0
- package/dist/patterns/AuthLayout.js +77 -0
- package/dist/patterns/AuthLayout.js.map +1 -0
- package/dist/patterns/CommentThread.js +9 -9
- package/dist/patterns/CommentThread.js.map +1 -1
- package/dist/patterns/DataImport.js +4 -4
- package/dist/patterns/DataImport.js.map +1 -1
- package/dist/patterns/DeadlineBadge.js +3 -3
- package/dist/patterns/DeadlineBadge.js.map +1 -1
- package/dist/patterns/FilterBar.d.ts +11 -1
- package/dist/patterns/FilterBar.js +39 -10
- package/dist/patterns/FilterBar.js.map +1 -1
- package/dist/patterns/FormDialog.d.ts +18 -0
- package/dist/patterns/FormDialog.js +46 -0
- package/dist/patterns/FormDialog.js.map +1 -0
- package/dist/patterns/KanbanBoard.js +8 -8
- package/dist/patterns/KanbanBoard.js.map +1 -1
- package/dist/patterns/LayoutSwitcher.d.ts +12 -0
- package/dist/patterns/LayoutSwitcher.js +79 -0
- package/dist/patterns/LayoutSwitcher.js.map +1 -0
- package/dist/patterns/NotificationCenter.js +8 -8
- package/dist/patterns/NotificationCenter.js.map +1 -1
- package/dist/patterns/PageTabs.d.ts +12 -7
- package/dist/patterns/PageTabs.js +41 -29
- package/dist/patterns/PageTabs.js.map +1 -1
- package/dist/patterns/QueryTable.d.ts +7 -1
- package/dist/patterns/QueryTable.js +116 -81
- package/dist/patterns/QueryTable.js.map +1 -1
- package/dist/patterns/SlideCaptcha.d.ts +80 -0
- package/dist/patterns/SlideCaptcha.js +354 -0
- package/dist/patterns/SlideCaptcha.js.map +1 -0
- package/dist/patterns/StepsForm.js +7 -7
- package/dist/patterns/StepsForm.js.map +1 -1
- package/dist/patterns/ThemeSwitcher.js +3 -3
- package/dist/patterns/ThemeSwitcher.js.map +1 -1
- package/dist/patterns/index.d.ts +7 -3
- package/dist/patterns/index.js +5 -1
- package/dist/patterns/messages.d.ts +37 -1
- package/dist/patterns/messages.js +38 -2
- package/dist/patterns/messages.js.map +1 -1
- package/dist/table/ColumnHeaderActions.js +3 -3
- package/dist/table/ColumnHeaderActions.js.map +1 -1
- package/dist/table/ColumnResizer.js +3 -3
- package/dist/table/ColumnResizer.js.map +1 -1
- package/dist/table/ColumnSettings.js +5 -5
- package/dist/table/ColumnSettings.js.map +1 -1
- package/dist/table/ColumnTable.js +8 -8
- package/dist/table/ColumnTable.js.map +1 -1
- package/dist/table/DataTable.js +8 -8
- package/dist/table/DataTable.js.map +1 -1
- package/dist/table/DataTableView.js +21 -21
- package/dist/table/DataTableView.js.map +1 -1
- package/dist/table/PivotTable.js +3 -3
- package/dist/table/PivotTable.js.map +1 -1
- package/dist/table/SelectionBar.js +4 -4
- package/dist/table/SelectionBar.js.map +1 -1
- package/dist/table/TreeDataTable.js +5 -5
- package/dist/table/TreeDataTable.js.map +1 -1
- package/dist/theme-customize/ThemeCustomizer.js +7 -6
- package/dist/theme-customize/ThemeCustomizer.js.map +1 -1
- package/dist/theme-customize/ThemeThumbnail.js +3 -3
- package/dist/theme-customize/ThemeThumbnail.js.map +1 -1
- package/dist/three/background/AuroraBackground.js +3 -3
- package/dist/three/background/AuroraBackground.js.map +1 -1
- package/dist/three/background/MeshGradient.js +3 -3
- package/dist/three/background/MeshGradient.js.map +1 -1
- package/dist/three/background/ShaderBackground.js +3 -3
- package/dist/three/background/ShaderBackground.js.map +1 -1
- package/dist/three/core/EffectSurface.js +3 -3
- package/dist/three/core/EffectSurface.js.map +1 -1
- package/dist/three/core/ThreeCanvas.js +4 -4
- package/dist/three/core/ThreeCanvas.js.map +1 -1
- package/dist/three/effects/Confetti.js +3 -3
- package/dist/three/effects/Confetti.js.map +1 -1
- package/dist/three/effects/ParticleField.js +3 -3
- package/dist/three/effects/ParticleField.js.map +1 -1
- package/dist/three/effects/WaveGrid.js +3 -3
- package/dist/three/effects/WaveGrid.js.map +1 -1
- package/dist/three/globe/Globe.js +3 -3
- package/dist/three/globe/Globe.js.map +1 -1
- package/dist/three/model/ModelViewer.js +3 -3
- package/dist/three/model/ModelViewer.js.map +1 -1
- package/dist/utils/data/number.d.ts +5 -0
- package/dist/utils/data/number.js +8 -1
- package/dist/utils/data/number.js.map +1 -1
- package/dist/utils/dom/element.d.ts +3 -1
- package/dist/utils/dom/element.js +8 -4
- package/dist/utils/dom/element.js.map +1 -1
- package/dist/utils/dom/fullscreen.d.ts +1 -1
- package/dist/utils/dom/fullscreen.js +12 -8
- package/dist/utils/dom/fullscreen.js.map +1 -1
- package/dist/utils/dom/mounted.d.ts +7 -0
- package/dist/utils/dom/mounted.js +16 -0
- package/dist/utils/dom/mounted.js.map +1 -0
- package/dist/utils/dom/overflow.js +2 -1
- package/dist/utils/dom/overflow.js.map +1 -1
- package/dist/utils/dom/pan-zoom.js +2 -1
- package/dist/utils/dom/pan-zoom.js.map +1 -1
- package/dist/utils/props.js +3 -13
- package/dist/utils/props.js.map +1 -1
- package/dist/utils/request/http.d.ts +12 -0
- package/dist/utils/request/http.js +6 -2
- package/dist/utils/request/http.js.map +1 -1
- package/dist/utils/request/use-request.js +4 -2
- package/dist/utils/request/use-request.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Listbox.js","names":["_isVNode","_vtp_readonlyProp","_vtp_restProps","_vue_shallowRef","_vue_computed","_createVNode","_mergeProps","_Fragment","Text","mergeProps","_vtp_bindable","_vtp_childrenProp"],"sources":["../../src/input/Listbox.tsx"],"sourcesContent":["/**\n * 列表框(role=\"listbox\"):在一列选项里单选或多选。数据驱动,始终虚拟滚动(上万个选项也只渲染看得见的几十个):\n *\n * <Listbox items={cities} _value={city} aria-label=\"城市\" />\n * <Listbox items={tags} selectionMode=\"multiple\" _value={picked}>{(tag) => <Badge>{tag.label}</Badge>}</Listbox>\n * <Listbox items={users} toOption={(user) => ({ value: user.id, label: user.name })} _value={id} aria-label=\"成员\" />\n *\n * 选项是 { value, label, disabled?, description?, icon?, group? }(ListboxOption);别的形状的数据用 toOption 转换,\n * 自定义内容的渲染函数拿到的仍是原始数据。\n * 焦点在列表上,高亮项经 aria-activedescendant 告诉读屏:↑↓、Home / End、Page Up / Down 移动,\n * Enter / 空格选中,按字母跳到以它开头的项。状态与键盘见 list-state.ts;Select、Combobox 用同一个 ListboxView 渲染。\n *\n * 虚拟滚动(useVirtualizer):高亮项总在渲染范围里(aria-activedescendant 才指得到),键盘移动时滚进可见范围;\n * 选项带 aria-setsize / aria-posinset,读屏知道总数与位置。分组标题只是视觉上的一行(各行独立定位,没法用\n * role=\"group\" 包住组里的选项),组名经 aria-describedby 交给组里的每个选项。\n * loading:没有选项时显示骨架行;已有选项(刷新中)时顶部显示一条进度条;列表带 aria-busy。\n *\n * 部件:Root(div,role=\"listbox\")、GroupLabel、Option(div,role=\"option\")、Check(已选标记)、Skeleton(加载中的占位行)。\n */\nimport { Check } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { useId, watch, withMemo, type VNode, type VNodeChild } from 'vue';\nimport { Icon, type IconProps } from '../base/Icon';\nimport { Text, type TextProps } from '../base/Text';\nimport { useDelayedLoading } from '../utils/motion/delayed-loading';\nimport { useLang } from '../config/context';\nimport { Empty, type EmptyProps } from '../feedback/Empty';\nimport { Progress } from '../feedback/Progress';\nimport { Skeleton, type SkeletonProps, type SkeletonShapeProps } from '../feedback/Skeleton';\nimport { SHIMMER_STAGGER } from '../utils/style/shimmer';\nimport { itemStyles } from '../utils/style/item-styles';\nimport {\n defaultLabelledBy,\n elementProps,\n mergeAttrs,\n mergePartProps,\n mergeProps,\n type PartProps,\n} from '../utils/props';\nimport { ScrollArea } from '../layout/ScrollArea';\nimport { selectedValues, type SelectionProps } from '../utils/data/selection';\nimport { focusRingStyles } from '../utils/style/styles';\nimport { useVirtualizer } from '../utils/virtual/use-virtualizer';\nimport { defaultRangeExtractor } from '../utils/virtual/virtualizer';\nimport { useFieldControl } from '../form/Field';\nimport {\n handleListKeydown,\n useListState,\n type ListboxOption,\n type ListStateEntry,\n type ListState,\n type OptionMapping,\n toListboxOption,\n} from './list-state';\n\n/** 选项的渲染状态:渲染函数据此画高亮、已选的样子 */\nexport interface OptionRenderState {\n /** 已选 */\n selected: boolean;\n /** 高亮(键盘或指针所在) */\n active: boolean;\n}\n\n/** 选项的部件属性:可以按选项(items 里的原始数据)计算 */\ntype OptionProps<T> =\n | PartProps<'div', 'role' | 'id' | 'aria-selected' | 'aria-disabled'>\n | ((item: T, state: OptionRenderState) => PartProps<'div'>);\n\n/** ListboxView 的属性:Listbox、Select、Combobox 共用的渲染层 */\nexport interface ListboxViewProps<T> extends PartProps<'div', 'role'> {\n /** 列表状态(useListState) */\n list: ListState<T>;\n /** 列表的 id;选项的 id 是 `${id}-${序号}`(aria-activedescendant 指向它) */\n id: string;\n /** 多选:aria-multiselectable */\n multiple?: boolean;\n /** 滚动容器:Select、Combobox 的弹出层;不给时列表自己滚动(ScrollArea 的浮层滚动条,最高 20em,用 sx 改) */\n scroller?: HTMLElement | null;\n /** 加载中:没有选项时显示骨架行,有选项时顶部显示进度条 */\n loading?: boolean;\n /** 选项的估计高度(px),默认 36;渲染后以实际高度为准 */\n estimateSize?: number;\n /** 自定义选项内容:拿到 items 里的原始数据 */\n renderOption?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 没有选项时显示的内容;默认是 Empty(文案取语言包) */\n empty?: VNodeChild;\n /** 点选了一项(点击或 Enter,再选已选的那项也会调用):Select、弹层里的列表据此关闭 */\n onPick?: (value: string) => void;\n /** 选项的部件属性 */\n propsOption?: OptionProps<T>;\n /** 分组标题的部件属性 */\n propsGroupLabel?: Partial<TextProps>;\n /** 已选标记的部件属性 */\n propsCheck?: Partial<IconProps>;\n /** 空状态的部件属性 */\n propsEmpty?: Partial<EmptyProps>;\n /** 加载中占位行的部件属性 */\n propsSkeleton?: SkeletonShapeProps;\n}\n\n/** 渲染用的一行:分组标题或选项 */\ntype Row<T> =\n | { kind: 'group'; key: string; label: string; id: string }\n | {\n kind: 'option';\n key: string;\n option: ListStateEntry<T>;\n index: number;\n groupId: string | undefined;\n };\n\n/** 加载中显示几行骨架 */\nconst SKELETON_ROWS = 4;\n\n/** Listbox 的样式:root、scroll、sizer、row、groupLabel、content 等 */\nexport const listboxStyles = css.create({\n root: { position: 'relative', padding: '_xs', lineHeight: '_normal', outlineStyle: 'none' },\n // 自己是滚动容器时(单独使用的 Listbox):限制高度,滚动不传给页面\n scroll: { maxHeight: '20em', overflowY: 'auto', overscrollBehavior: 'contain' },\n sizer: { position: 'relative', width: '_full' },\n row: { position: 'absolute', top: 0, insetInline: 0 },\n groupLabel: {\n display: 'block',\n paddingTop: '_sm',\n paddingBottom: '_xs',\n paddingInline: '_sm',\n fontWeight: '_semibold',\n },\n content: { display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0 },\n description: { color: '_textMuted' },\n check: { flexShrink: 0 },\n hidden: { visibility: 'hidden' },\n // 刷新中的进度条贴在列表顶部,不占位\n progress: { position: 'absolute', top: 0, insetInline: 0, zIndex: 1 },\n skeleton: { display: 'block', height: '1em', marginBlock: '0.625em', marginInline: '_sm' },\n});\n\n// 行的位置是任意值,用动态样式;单独一个 create,见 COMPONENTS.md §4\n/** Listbox 的样式:y、height、width */\nexport const listboxPositionStyles = css.create({\n y: (px: number) => ({ transform: `translateY(${px}px)` }),\n height: (px: number) => ({ height: px }),\n width: (percent: string) => ({ width: percent }),\n});\n\n/** 列表的渲染层:虚拟滚动的选项、分组标题、已选标记、空状态与加载中;不管焦点,指针悬停高亮、点击选中 */\nexport const ListboxView = _component(\n <T,>({\n list,\n id,\n multiple = false,\n scroller,\n loading = false,\n estimateSize = 36,\n renderOption,\n empty,\n onPick,\n propsOption,\n propsGroupLabel,\n propsCheck,\n propsEmpty,\n propsSkeleton,\n ...rest\n }: ListboxViewProps<T>) => {\n const lang = useLang();\n // 骨架、进度条防闪烁;aria-busy 按真实的 loading\n const indicator = useDelayedLoading(() => loading);\n let root = _state.raw<HTMLElement | null>(null);\n let sizer = _state.raw<HTMLElement | null>(null);\n\n /** 选项按分组排好后展开成行:每组前插一行标题 */\n const rows = _derived.by<Row<T>[]>(() => {\n const result: Row<T>[] = [];\n let group: string | undefined;\n let headerId: string | undefined;\n list.options.forEach((option, index) => {\n if (option.group != null && option.group !== group) {\n headerId = `${id}-group-${result.length}`;\n result.push({\n kind: 'group',\n key: `group:${option.group}`,\n label: option.group,\n id: headerId,\n });\n }\n group = option.group;\n result.push({\n kind: 'option',\n key: option.value,\n option,\n index,\n groupId: option.group != null ? headerId : undefined,\n });\n });\n return result;\n });\n\n /** 值 → 行号:高亮项的行号一次查到,不在每次渲染时扫一遍 */\n const rowOf = _derived.by(() => {\n const map = new Map<string, number>();\n rows.forEach((row, index) => {\n if (row.kind === 'option') map.set(row.option.value, index);\n });\n return map;\n });\n const activeRow = () => (list.active === undefined ? -1 : (rowOf.get(list.active) ?? -1));\n\n const virtual = useVirtualizer(() => {\n const current = rows;\n // 读一下高亮项:它变了要重新算渲染范围(高亮项总要渲染)\n const active = activeRow();\n // 先读出来:每一项都要调用一次估计高度,不要每次都经过 props 的响应式读取\n const size = estimateSize;\n return {\n count: current.length,\n scrollElement: scroller === undefined ? root : scroller,\n list: sizer,\n estimateSize: (index: number) => (current[index]?.kind === 'group' ? 30 : size),\n getItemKey: (index: number) => current[index]!.key,\n overscan: 6,\n rangeExtractor: (range) => {\n const indices = defaultRangeExtractor(range);\n if (active >= 0 && !indices.includes(active)) {\n indices.push(active);\n indices.sort((a, b) => a - b);\n }\n return indices;\n },\n };\n });\n\n // 高亮项变了(键盘移动)时滚进可见范围;已经完整可见就不动(指针悬停触发的不会滚)。\n // 高亮的是第一项、上面是组名时滚到最顶上:组名不被挡住(搜索过滤后高亮第一个匹配的)\n watch(\n () => list.active,\n () => {\n const index = activeRow();\n if (index < 0) return;\n virtual.scrollToIndex(index === 1 && rows[0]?.kind === 'group' ? 0 : index, {\n align: 'auto',\n });\n },\n { flush: 'post' },\n );\n\n /**\n * 选项行按“会影响它的值”缓存(Vue 的 withMemo,同模板的 v-memo):方向键移动高亮时只重建前后两行,\n * 不再重建全部可见行与其中的图标。自定义了 renderOption 或按项计算的 propsOption 时不缓存:\n * 使用方的函数可能读别的状态,缓存了会显示旧内容\n */\n const memoCache: VNode[] = [];\n function optionRow(row: Extract<Row<T>, { kind: 'option' }>) {\n if (renderOption || typeof propsOption === 'function') return renderOptionRow(row);\n const { option } = row;\n return withMemo(\n [\n option,\n row.index,\n row.groupId,\n list.isSelected(option.value),\n list.active === option.value,\n list.options.length,\n propsOption,\n propsCheck,\n ],\n () => renderOptionRow(row),\n memoCache,\n row.index,\n );\n }\n\n function renderOptionRow(row: Extract<Row<T>, { kind: 'option' }>) {\n const { option, index } = row;\n const state = {\n selected: list.isSelected(option.value),\n active: list.active === option.value,\n };\n const own = typeof propsOption === 'function' ? propsOption(option.item, state) : propsOption;\n return (\n <div\n {...mergeAttrs<'div'>(\n {\n sx: [\n itemStyles.row,\n state.active && itemStyles.highlighted,\n state.selected && itemStyles.selected,\n option.disabled && itemStyles.disabled,\n ],\n },\n own,\n {\n id: `${id}-${index}`,\n role: 'option',\n 'aria-selected': state.selected,\n 'aria-disabled': option.disabled || undefined,\n 'aria-setsize': list.options.length,\n 'aria-posinset': index + 1,\n 'aria-describedby': row.groupId,\n // 焦点留在列表、触发按钮或输入框上\n onPointerdown: (event: PointerEvent) => event.preventDefault(),\n onPointermove: () => {\n if (!option.disabled && list.active !== option.value) list.setActive(option.value);\n },\n onClick: () => {\n if (option.disabled) return;\n list.select(option.value);\n onPick?.(option.value);\n },\n },\n )}\n data-state={state.selected ? 'checked' : 'unchecked'}\n data-active={state.active || undefined}\n data-disabled={option.disabled || undefined}\n >\n {renderOption ? (\n renderOption(option.item, state)\n ) : (\n <>\n {option.icon && <Icon icon={option.icon} />}\n <span {...css.attrs(listboxStyles.content)}>\n <span {...css.attrs(itemStyles.label)}>{option.label}</span>\n {option.description && (\n <Text size=\"sm\" sx={listboxStyles.description}>\n {option.description}\n </Text>\n )}\n </span>\n </>\n )}\n <Icon\n {...mergePartProps<IconProps>(\n { icon: Check, sx: [listboxStyles.check, !state.selected && listboxStyles.hidden] },\n propsCheck,\n )}\n />\n </div>\n );\n }\n\n function renderBody() {\n if (rows.length === 0) {\n if (loading) {\n if (!indicator.value) return null;\n return Array.from({ length: SKELETON_ROWS }, (_, index) => (\n <Skeleton\n key={index}\n {...mergePartProps<SkeletonProps>(\n {\n sx: [listboxStyles.skeleton, listboxPositionStyles.width(`${85 - index * 15}%`)],\n delay: index * SHIMMER_STAGGER,\n },\n propsSkeleton,\n )}\n />\n ));\n }\n return (\n empty ?? (\n <Empty\n {...mergePartProps<EmptyProps>(\n { size: 'sm', icon: false, description: lang.selectEmpty },\n propsEmpty,\n )}\n />\n )\n );\n }\n return (\n <div\n ref={_toRef(sizer)}\n role=\"presentation\"\n {...css.attrs(listboxStyles.sizer, listboxPositionStyles.height(virtual.total))}\n >\n {virtual.items.map((item) => {\n const row = rows[item.index]!;\n return (\n <div\n key={item.key}\n ref={virtual.measure}\n data-index={item.index}\n role=\"presentation\"\n {...css.attrs(listboxStyles.row, listboxPositionStyles.y(item.start))}\n >\n {row.kind === 'group' ? (\n <Text\n {...mergePartProps<TextProps>(\n { tone: 'muted', size: 'sm', sx: listboxStyles.groupLabel },\n propsGroupLabel,\n // 只是视觉上的标题:组名经各选项的 aria-describedby 念出\n { id: row.id, 'aria-hidden': true },\n )}\n >\n {row.label}\n </Text>\n ) : (\n optionRow(row)\n )}\n </div>\n );\n })}\n </div>\n );\n }\n\n return () => {\n const own = scroller === undefined;\n // 先不编译 sx:自己滚动时这些属性交给 ScrollArea 的视口,和视口自己的样式一次合并\n const props = mergeProps<'div'>(\n { sx: [listboxStyles.root, own && listboxStyles.scroll, focusRingStyles.visible] },\n rest,\n {\n ref: _toRef(root),\n id,\n role: 'listbox',\n 'aria-multiselectable': multiple || undefined,\n 'aria-busy': loading || undefined,\n },\n );\n const body = [\n indicator.value && rows.length > 0 && (\n <div aria-hidden=\"true\" {...css.attrs(listboxStyles.progress)}>\n <Progress size=\"xs\" aria-label={lang.loading} />\n </div>\n ),\n renderBody(),\n ];\n // 别处滚动(Select、Combobox 的弹出层):只渲染列表本身\n if (!own) return <div {...elementProps(props)}>{body}</div>;\n // 自己滚动:列表元素就是 ScrollArea 的视口,焦点与 aria-activedescendant 仍在它上面,滚动条换成浮层的\n return (\n <ScrollArea focusable={false} propsViewport={props as PartProps<'div'>}>\n {body}\n </ScrollArea>\n );\n };\n },\n { name: 'ListboxView' },\n);\n\n/** 高亮项的 id(给 aria-activedescendant);没有高亮时 undefined */\nexport function activeOptionId<T>(list: ListState<T>, id: string): string | undefined {\n const index = list.active === undefined ? -1 : list.indexOf(list.active);\n return index < 0 ? undefined : `${id}-${index}`;\n}\n\n/** 列表框共有的属性;未声明的属性落到 role=\"listbox\" 的 div 上 */\nexport interface ListboxBaseProps<T> extends Omit<\n ListboxViewProps<T>,\n 'list' | 'id' | 'multiple' | 'renderOption'\n> {\n /** 列表的 id;在 Field 里时默认用 Field 给的 */\n id?: string;\n /** 数据:ListboxOption、字符串,或任意数据加 toOption */\n items: readonly T[];\n /** 必须有选中项:单选时再点已选项不取消;在必填的 Field 里默认开启 */\n required?: boolean;\n /** 不可用 */\n disabled?: boolean;\n /** 自定义选项内容:`{(item, { selected, active }) => …}`,拿到 items 里的原始数据 */\n children?: (item: T, state: OptionRenderState) => VNodeChild;\n}\n\n/** 单选或多选;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type ListboxProps<T = ListboxOption> = ListboxBaseProps<T> &\n SelectionProps &\n OptionMapping<T>;\n\n/** 列表框:焦点在列表上,方向键移动高亮、Enter / 空格选中 */\nexport const Listbox = _component(\n <T,>({\n items,\n toOption,\n selectionMode = 'single',\n _value: value = _bindable<string | string[] | null>(null),\n required,\n disabled,\n id,\n children,\n onPick,\n ...rest\n }: ListboxProps<T>) => {\n const generatedId = useId();\n const control = useFieldControl(() => ({ id, disabled, required }));\n const list = useListState({\n items: () => items,\n toOption: (item) => toListboxOption(item, toOption),\n selected: () => selectedValues(value),\n mode: () => selectionMode,\n required: () => control().required,\n onChange: (next) => (value = selectionMode === 'multiple' ? [...next] : (next[0] ?? null)),\n });\n\n function onKeydown(event: KeyboardEvent) {\n if (control().disabled) return;\n if (handleListKeydown(list, event, { space: true, typeahead: true }, onPick)) {\n event.preventDefault();\n }\n }\n\n return () => {\n const state = control();\n const prefix = state.id ?? generatedId;\n return (\n <ListboxView\n {...mergePartProps<ListboxViewProps<T>>(\n {\n list,\n id: prefix,\n multiple: selectionMode === 'multiple',\n renderOption: children,\n onPick,\n },\n rest as Partial<ListboxViewProps<T>>,\n {\n id: prefix,\n tabindex: state.disabled ? -1 : 0,\n 'aria-labelledby': defaultLabelledBy(rest, state.labelledBy),\n 'aria-describedby': state.describedBy,\n 'aria-disabled': state.disabled || undefined,\n 'aria-required': state.required || undefined,\n 'aria-activedescendant': activeOptionId(list, prefix),\n onFocus: () => {\n if (list.active === undefined) list.activateDefault();\n },\n onKeydown,\n },\n )}\n />\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Listbox.js","names":["_isVNode","_vtp_readonlyProp","_vtp_restProps","_vue_shallowRef","_vue_computed","_createVNode","_mergeProps","_Fragment","Text","_vtp_bindable","_vtp_childrenProp"],"sources":["../../src/input/Listbox.tsx"],"sourcesContent":["/**\n * 列表框(role=\"listbox\"):在一列选项里单选或多选。数据驱动,始终虚拟滚动(上万个选项也只渲染看得见的几十个):\n *\n * <Listbox items={cities} _value={city} aria-label=\"城市\" />\n * <Listbox items={tags} selectionMode=\"multiple\" _value={picked}>{(tag) => <Badge>{tag.label}</Badge>}</Listbox>\n * <Listbox items={users} toOption={(user) => ({ value: user.id, label: user.name })} _value={id} aria-label=\"成员\" />\n *\n * 选项是 { value, label, disabled?, description?, icon?, group? }(ListboxOption);别的形状的数据用 toOption 转换,\n * 自定义内容的渲染函数拿到的仍是原始数据。\n * 焦点在列表上,高亮项经 aria-activedescendant 告诉读屏:↑↓、Home / End、Page Up / Down 移动,\n * Enter / 空格选中,按字母跳到以它开头的项。状态与键盘见 list-state.ts;Select、Combobox 用同一个 ListboxView 渲染。\n *\n * 虚拟滚动(useVirtualizer):高亮项总在渲染范围里(aria-activedescendant 才指得到),键盘移动时滚进可见范围;\n * 选项带 aria-setsize / aria-posinset,读屏知道总数与位置。分组标题只是视觉上的一行(各行独立定位,没法用\n * role=\"group\" 包住组里的选项),组名经 aria-describedby 交给组里的每个选项。\n * loading:没有选项时显示骨架行;已有选项(刷新中)时顶部显示一条进度条;列表带 aria-busy。\n *\n * 部件:Root(div,role=\"listbox\")、GroupLabel、Option(div,role=\"option\")、Check(已选标记)、Skeleton(加载中的占位行)。\n */\nimport { Check } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { useId, watch, withMemo, type VNode, type VNodeChild } from 'vue';\nimport { Icon, type IconProps } from '../base/Icon';\nimport { Text, type TextProps } from '../base/Text';\nimport { useDelayedLoading } from '../utils/motion/delayed-loading';\nimport { useLang } from '../config/context';\nimport { Empty, type EmptyProps } from '../feedback/Empty';\nimport { Progress } from '../feedback/Progress';\nimport { Skeleton, type SkeletonProps, type SkeletonShapeProps } from '../feedback/Skeleton';\nimport { SHIMMER_STAGGER } from '../utils/style/shimmer';\nimport { itemStyles } from '../utils/style/item-styles';\nimport {\n defaultLabelledBy,\n elementProps,\n mergeAttrs,\n mergePartProps,\n mergeProps,\n type PartProps,\n} from '../utils/props';\nimport { ScrollArea } from '../layout/ScrollArea';\nimport { selectedValues, type SelectionProps } from '../utils/data/selection';\nimport { focusRingStyles } from '../utils/style/styles';\nimport { useVirtualizer } from '../utils/virtual/use-virtualizer';\nimport { defaultRangeExtractor } from '../utils/virtual/virtualizer';\nimport { useFieldControl } from '../form/Field';\nimport {\n handleListKeydown,\n useListState,\n type ListboxOption,\n type ListStateEntry,\n type ListState,\n type OptionMapping,\n toListboxOption,\n} from './list-state';\n\n/** 选项的渲染状态:渲染函数据此画高亮、已选的样子 */\nexport interface OptionRenderState {\n /** 已选 */\n selected: boolean;\n /** 高亮(键盘或指针所在) */\n active: boolean;\n}\n\n/** 选项的部件属性:可以按选项(items 里的原始数据)计算 */\ntype OptionProps<T> =\n | PartProps<'div', 'role' | 'id' | 'aria-selected' | 'aria-disabled'>\n | ((item: T, state: OptionRenderState) => PartProps<'div'>);\n\n/** ListboxView 的属性:Listbox、Select、Combobox 共用的渲染层 */\nexport interface ListboxViewProps<T> extends PartProps<'div', 'role'> {\n /** 列表状态(useListState) */\n list: ListState<T>;\n /** 列表的 id;选项的 id 是 `${id}-${序号}`(aria-activedescendant 指向它) */\n id: string;\n /** 多选:aria-multiselectable */\n multiple?: boolean;\n /** 滚动容器:Select、Combobox 的弹出层;不给时列表自己滚动(ScrollArea 的浮层滚动条,最高 20em,用 sx 改) */\n scroller?: HTMLElement | null;\n /** 加载中:没有选项时显示骨架行,有选项时顶部显示进度条 */\n loading?: boolean;\n /** 选项的估计高度(px),默认 36;渲染后以实际高度为准 */\n estimateSize?: number;\n /** 自定义选项内容:拿到 items 里的原始数据 */\n renderOption?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 没有选项时显示的内容;默认是 Empty(文案取语言包) */\n empty?: VNodeChild;\n /** 点选了一项(点击或 Enter,再选已选的那项也会调用):Select、弹层里的列表据此关闭 */\n onPick?: (value: string) => void;\n /** 选项的部件属性 */\n propsOption?: OptionProps<T>;\n /** 分组标题的部件属性 */\n propsGroupLabel?: Partial<TextProps>;\n /** 已选标记的部件属性 */\n propsCheck?: Partial<IconProps>;\n /** 空状态的部件属性 */\n propsEmpty?: Partial<EmptyProps>;\n /** 加载中占位行的部件属性 */\n propsSkeleton?: SkeletonShapeProps;\n}\n\n/** 渲染用的一行:分组标题或选项 */\ntype Row<T> =\n | { kind: 'group'; key: string; label: string; id: string }\n | {\n kind: 'option';\n key: string;\n option: ListStateEntry<T>;\n index: number;\n groupId: string | undefined;\n };\n\n/** 加载中显示几行骨架 */\nconst SKELETON_ROWS = 4;\n\n/** Listbox 的样式:root、scroll、sizer、row、groupLabel、content 等 */\nexport const listboxStyles = css.create({\n root: { position: 'relative', padding: '_xs', lineHeight: '_normal', outlineStyle: 'none' },\n // 自己是滚动容器时(单独使用的 Listbox):限制高度,滚动不传给页面\n scroll: { maxHeight: '20em', overflowY: 'auto', overscrollBehavior: 'contain' },\n sizer: { position: 'relative', width: '_full' },\n row: { position: 'absolute', top: 0, insetInline: 0 },\n groupLabel: {\n display: 'block',\n paddingTop: '_sm',\n paddingBottom: '_xs',\n paddingInline: '_sm',\n fontWeight: '_semibold',\n },\n content: { display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0 },\n description: { color: '_textMuted' },\n check: { flexShrink: 0 },\n hidden: { visibility: 'hidden' },\n // 刷新中的进度条贴在列表顶部,不占位\n progress: { position: 'absolute', top: 0, insetInline: 0, zIndex: 1 },\n skeleton: { display: 'block', height: '1em', marginBlock: '0.625em', marginInline: '_sm' },\n});\n\n// 行的位置是任意值,用动态样式;单独一个 create,见 COMPONENTS.md §4\n/** Listbox 的样式:y、height、width */\nexport const listboxPositionStyles = css.create({\n y: (px: number) => ({ transform: `translateY(${px}px)` }),\n height: (px: number) => ({ height: px }),\n width: (percent: string) => ({ width: percent }),\n});\n\n/** 列表的渲染层:虚拟滚动的选项、分组标题、已选标记、空状态与加载中;不管焦点,指针悬停高亮、点击选中 */\nexport const ListboxView = _component(\n <T,>({\n list,\n id,\n multiple = false,\n scroller,\n loading = false,\n estimateSize = 36,\n renderOption,\n empty,\n onPick,\n propsOption,\n propsGroupLabel,\n propsCheck,\n propsEmpty,\n propsSkeleton,\n ...rest\n }: ListboxViewProps<T>) => {\n const lang = useLang();\n // 骨架、进度条防闪烁;aria-busy 按真实的 loading\n const indicator = useDelayedLoading(() => loading);\n let root = _state.raw<HTMLElement | null>(null);\n let sizer = _state.raw<HTMLElement | null>(null);\n\n /** 选项按分组排好后展开成行:每组前插一行标题 */\n const rows = _derived.by<Row<T>[]>(() => {\n const result: Row<T>[] = [];\n let group: string | undefined;\n let headerId: string | undefined;\n list.options.forEach((option, index) => {\n if (option.group != null && option.group !== group) {\n headerId = `${id}-group-${result.length}`;\n result.push({\n kind: 'group',\n key: `group:${option.group}`,\n label: option.group,\n id: headerId,\n });\n }\n group = option.group;\n result.push({\n kind: 'option',\n key: option.value,\n option,\n index,\n groupId: option.group != null ? headerId : undefined,\n });\n });\n return result;\n });\n\n /** 值 → 行号:高亮项的行号一次查到,不在每次渲染时扫一遍 */\n const rowOf = _derived.by(() => {\n const map = new Map<string, number>();\n rows.forEach((row, index) => {\n if (row.kind === 'option') map.set(row.option.value, index);\n });\n return map;\n });\n const activeRow = () => (list.active === undefined ? -1 : (rowOf.get(list.active) ?? -1));\n\n const virtual = useVirtualizer(() => {\n const current = rows;\n // 读一下高亮项:它变了要重新算渲染范围(高亮项总要渲染)\n const active = activeRow();\n // 先读出来:每一项都要调用一次估计高度,不要每次都经过 props 的响应式读取\n const size = estimateSize;\n return {\n count: current.length,\n scrollElement: scroller === undefined ? root : scroller,\n list: sizer,\n estimateSize: (index: number) => (current[index]?.kind === 'group' ? 30 : size),\n getItemKey: (index: number) => current[index]!.key,\n overscan: 6,\n rangeExtractor: (range) => {\n const indices = defaultRangeExtractor(range);\n if (active >= 0 && !indices.includes(active)) {\n indices.push(active);\n indices.sort((a, b) => a - b);\n }\n return indices;\n },\n };\n });\n\n // 高亮项变了(键盘移动)时滚进可见范围;已经完整可见就不动(指针悬停触发的不会滚)。\n // 高亮的是第一项、上面是组名时滚到最顶上:组名不被挡住(搜索过滤后高亮第一个匹配的)\n watch(\n () => list.active,\n () => {\n const index = activeRow();\n if (index < 0) return;\n virtual.scrollToIndex(index === 1 && rows[0]?.kind === 'group' ? 0 : index, {\n align: 'auto',\n });\n },\n { flush: 'post' },\n );\n\n /**\n * 选项行按“会影响它的值”缓存(Vue 的 withMemo,同模板的 v-memo):方向键移动高亮时只重建前后两行,\n * 不再重建全部可见行与其中的图标。自定义了 renderOption 或按项计算的 propsOption 时不缓存:\n * 使用方的函数可能读别的状态,缓存了会显示旧内容\n */\n const memoCache: VNode[] = [];\n function optionRow(row: Extract<Row<T>, { kind: 'option' }>) {\n if (renderOption || typeof propsOption === 'function') return renderOptionRow(row);\n const { option } = row;\n return withMemo(\n [\n option,\n row.index,\n row.groupId,\n list.isSelected(option.value),\n list.active === option.value,\n list.options.length,\n propsOption,\n propsCheck,\n ],\n () => renderOptionRow(row),\n memoCache,\n row.index,\n );\n }\n\n function renderOptionRow(row: Extract<Row<T>, { kind: 'option' }>) {\n const { option, index } = row;\n const state = {\n selected: list.isSelected(option.value),\n active: list.active === option.value,\n };\n const own = typeof propsOption === 'function' ? propsOption(option.item, state) : propsOption;\n return (\n <div\n {...mergeAttrs<'div'>(\n {\n sx: [\n itemStyles.row,\n state.active && itemStyles.highlighted,\n state.selected && itemStyles.selected,\n option.disabled && itemStyles.disabled,\n ],\n },\n own,\n {\n id: `${id}-${index}`,\n role: 'option',\n 'aria-selected': state.selected,\n 'aria-disabled': option.disabled || undefined,\n 'aria-setsize': list.options.length,\n 'aria-posinset': index + 1,\n 'aria-describedby': row.groupId,\n // 焦点留在列表、触发按钮或输入框上\n onPointerdown: (event: PointerEvent) => event.preventDefault(),\n onPointermove: () => {\n if (!option.disabled && list.active !== option.value) list.setActive(option.value);\n },\n onClick: () => {\n if (option.disabled) return;\n list.select(option.value);\n onPick?.(option.value);\n },\n },\n )}\n data-state={state.selected ? 'checked' : 'unchecked'}\n data-active={state.active || undefined}\n data-disabled={option.disabled || undefined}\n >\n {renderOption ? (\n renderOption(option.item, state)\n ) : (\n <>\n {option.icon && <Icon icon={option.icon} />}\n <span {...css.attrs(listboxStyles.content)}>\n <span {...css.attrs(itemStyles.label)}>{option.label}</span>\n {option.description && (\n <Text size=\"sm\" sx={listboxStyles.description}>\n {option.description}\n </Text>\n )}\n </span>\n </>\n )}\n <Icon\n {...mergePartProps<IconProps>(\n { icon: Check, sx: [listboxStyles.check, !state.selected && listboxStyles.hidden] },\n propsCheck,\n )}\n />\n </div>\n );\n }\n\n function renderBody() {\n if (rows.length === 0) {\n if (loading) {\n if (!indicator.value) return null;\n return Array.from({ length: SKELETON_ROWS }, (_, index) => (\n <Skeleton\n key={index}\n {...mergePartProps<SkeletonProps>(\n {\n sx: [listboxStyles.skeleton, listboxPositionStyles.width(`${85 - index * 15}%`)],\n delay: index * SHIMMER_STAGGER,\n },\n propsSkeleton,\n )}\n />\n ));\n }\n return (\n empty ?? (\n <Empty\n {...mergePartProps<EmptyProps>(\n { size: 'sm', icon: false, description: lang.selectEmpty },\n propsEmpty,\n )}\n />\n )\n );\n }\n return (\n <div\n ref={_toRef(sizer)}\n role=\"presentation\"\n {...css.attrs(listboxStyles.sizer, listboxPositionStyles.height(virtual.total))}\n >\n {virtual.items.map((item) => {\n const row = rows[item.index]!;\n return (\n <div\n key={item.key}\n ref={virtual.measure}\n data-index={item.index}\n role=\"presentation\"\n {...css.attrs(listboxStyles.row, listboxPositionStyles.y(item.start))}\n >\n {row.kind === 'group' ? (\n <Text\n {...mergePartProps<TextProps>(\n { tone: 'muted', size: 'sm', sx: listboxStyles.groupLabel },\n propsGroupLabel,\n // 只是视觉上的标题:组名经各选项的 aria-describedby 念出\n { id: row.id, 'aria-hidden': true },\n )}\n >\n {row.label}\n </Text>\n ) : (\n optionRow(row)\n )}\n </div>\n );\n })}\n </div>\n );\n }\n\n return () => {\n const own = scroller === undefined;\n // 先不编译 sx:自己滚动时这些属性交给 ScrollArea 的视口,和视口自己的样式一次合并\n const props = mergeProps<'div'>(\n { sx: [listboxStyles.root, own && listboxStyles.scroll, focusRingStyles.visible] },\n rest,\n {\n ref: _toRef(root),\n id,\n role: 'listbox',\n 'aria-multiselectable': multiple || undefined,\n 'aria-busy': loading || undefined,\n },\n );\n const body = [\n indicator.value && rows.length > 0 && (\n <div aria-hidden=\"true\" {...css.attrs(listboxStyles.progress)}>\n <Progress size=\"xs\" aria-label={lang.loading} />\n </div>\n ),\n renderBody(),\n ];\n // 别处滚动(Select、Combobox 的弹出层):只渲染列表本身\n if (!own) return <div {...elementProps(props)}>{body}</div>;\n // 自己滚动:列表元素就是 ScrollArea 的视口,焦点与 aria-activedescendant 仍在它上面,滚动条换成浮层的\n return (\n <ScrollArea focusable={false} propsViewport={props as PartProps<'div'>}>\n {body}\n </ScrollArea>\n );\n };\n },\n { name: 'ListboxView' },\n);\n\n/** 高亮项的 id(给 aria-activedescendant);没有高亮时 undefined */\nexport function activeOptionId<T>(list: ListState<T>, id: string): string | undefined {\n const index = list.active === undefined ? -1 : list.indexOf(list.active);\n return index < 0 ? undefined : `${id}-${index}`;\n}\n\n/** 列表框共有的属性;未声明的属性落到 role=\"listbox\" 的 div 上 */\nexport interface ListboxBaseProps<T> extends Omit<\n ListboxViewProps<T>,\n 'list' | 'id' | 'multiple' | 'renderOption'\n> {\n /** 列表的 id;在 Field 里时默认用 Field 给的 */\n id?: string;\n /** 数据:ListboxOption、字符串,或任意数据加 toOption */\n items: readonly T[];\n /** 必须有选中项:单选时再点已选项不取消;在必填的 Field 里默认开启 */\n required?: boolean;\n /** 不可用 */\n disabled?: boolean;\n /** 自定义选项内容:`{(item, { selected, active }) => …}`,拿到 items 里的原始数据 */\n children?: (item: T, state: OptionRenderState) => VNodeChild;\n}\n\n/** 单选或多选;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type ListboxProps<T = ListboxOption> = ListboxBaseProps<T> &\n SelectionProps &\n OptionMapping<T>;\n\n/** 列表框:焦点在列表上,方向键移动高亮、Enter / 空格选中 */\nexport const Listbox = _component(\n <T,>({\n items,\n toOption,\n selectionMode = 'single',\n _value: value = _bindable<string | string[] | null>(null),\n required,\n disabled,\n id,\n children,\n onPick,\n ...rest\n }: ListboxProps<T>) => {\n const generatedId = useId();\n const control = useFieldControl(() => ({ id, disabled, required }));\n const list = useListState({\n items: () => items,\n toOption: (item) => toListboxOption(item, toOption),\n selected: () => selectedValues(value),\n mode: () => selectionMode,\n required: () => control().required,\n onChange: (next) => (value = selectionMode === 'multiple' ? [...next] : (next[0] ?? null)),\n });\n\n function onKeydown(event: KeyboardEvent) {\n if (control().disabled) return;\n if (handleListKeydown(list, event, { space: true, typeahead: true }, onPick)) {\n event.preventDefault();\n }\n }\n\n return () => {\n const state = control();\n const prefix = state.id ?? generatedId;\n return (\n <ListboxView\n {...mergePartProps<ListboxViewProps<T>>(\n {\n list,\n id: prefix,\n multiple: selectionMode === 'multiple',\n renderOption: children,\n onPick,\n },\n rest as Partial<ListboxViewProps<T>>,\n {\n id: prefix,\n tabindex: state.disabled ? -1 : 0,\n 'aria-labelledby': defaultLabelledBy(rest, state.labelledBy),\n 'aria-describedby': state.describedBy,\n 'aria-disabled': state.disabled || undefined,\n 'aria-required': state.required || undefined,\n 'aria-activedescendant': activeOptionId(list, prefix),\n onFocus: () => {\n if (list.active === undefined) list.activateDefault();\n },\n onKeydown,\n },\n )}\n />\n );\n };\n },\n);\n"],"mappings":"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"}
|
package/dist/input/Mentions.js
CHANGED
|
@@ -9,8 +9,8 @@ import { activeOptionId } from "./Listbox.js";
|
|
|
9
9
|
import { ListPopover } from "./ListPopover.js";
|
|
10
10
|
import { useRemoteItems } from "./remote-items.js";
|
|
11
11
|
import { caretAnchor } from "../utils/dom/caret.js";
|
|
12
|
-
import { Fragment, createVNode,
|
|
13
|
-
import { _component, bindable, childrenProp, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
12
|
+
import { Fragment, createVNode, nextTick, ref, shallowRef, useId, watch } from "vue";
|
|
13
|
+
import { _component, bindable, childrenProp, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
14
14
|
import * as css from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
15
15
|
//#region src/input/Mentions.tsx
|
|
16
16
|
/** 提及的弹出列表由组件管理 */
|
|
@@ -230,7 +230,7 @@ var Mentions = /*#__PURE__*/ _component((_props2, _ctx2) => {
|
|
|
230
230
|
if (reason === "escape") dismissed = trigger.value;
|
|
231
231
|
setOpen(false, reason, event);
|
|
232
232
|
} });
|
|
233
|
-
return () => createVNode(Fragment, null, [createVNode(Textarea,
|
|
233
|
+
return () => createVNode(Fragment, null, [createVNode(Textarea, mergeJsxProps(mergePartProps({ propsRoot: mergePartProps({}, propsRoot.value, { ref: shell }) }, rest, {
|
|
234
234
|
"aria-autocomplete": "list",
|
|
235
235
|
"aria-controls": open.value ? listId : void 0,
|
|
236
236
|
"aria-activedescendant": open.value ? activeOptionId(list, listId) : void 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Mentions.js","names":["_vtp_readonlyProp","_vtp_restProps","_createVNode","_vtp_bindable","_vtp_childrenProp","_vue_shallowRef","_vue_ref","_Fragment","_mergeProps"],"sources":["../../src/input/Mentions.tsx"],"sourcesContent":["/**\n * 提及:多行文本框里输入 @ 弹出人员(或岗位、群组)列表,选中后写成“@名字 ”。留言、审批意见、协商回复里通知相关的人。\n *\n * <Field label=\"留言\"><Mentions _value={text} items={people} _mentions={notified} /></Field>\n * <Mentions _value={text} load={(query, signal) => api.searchUsers(query, { signal })} toOption={…} />\n *\n * 值仍是文字;mentions 是文字里提到的选项的值(工号、岗位编号),随文字增删——删掉“@名字”就不再算提及。\n * 显示提及过的文字用 MentionText(提及高亮),拆分用 mentionSegments。\n *\n * - 弹出:@ 在开头或前面不是字母数字(邮箱里的 @ 不算),@ 与光标之间没有空白;列表贴在 @ 的位置(caretAnchor)。\n * - 过滤:本地按显示文字包含 @ 后面的文字;给 load 时按它远程搜索(同 Combobox)。\n * - 键盘:↑↓ 移动、Enter 或 Tab 选中、Esc 关闭(这个 @ 不再弹出,直到重新输入);焦点一直在文本框里,\n * 高亮项经 aria-activedescendant 告诉读屏,结果数量播报。中文输入法组合期间不弹出、不过滤。\n *\n * 外壳与自动增高同 Textarea。部件:Root(外壳)、Textarea、Popover、Listbox。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { nextTick, useId, watch, type VNodeChild } from 'vue';\nimport { useLang, useTextMatcher } from '../config/context';\nimport { caretAnchor } from '../utils/dom/caret';\nimport { useAnchoredPopover } from '../utils/dom/floating';\nimport { isComposingKey } from '../utils/dom/hotkeys';\nimport { openSetter, type OpenChangeProps } from '../utils/dom/open-change';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { activeOptionId, type ListboxViewProps, type OptionRenderState } from './Listbox';\nimport { ListPopover } from './ListPopover';\nimport {\n handleListKeydown,\n toListboxOption,\n useListState,\n useResultAnnouncement,\n type ListboxOption,\n type OptionMapping,\n} from './list-state';\nimport { useRemoteItems, type OptionsLoader } from './remote-items';\nimport { Textarea, type TextareaProps } from './Textarea';\n\n/** 提及的弹出列表由组件管理 */\nexport type MentionsManaged = 'aria-autocomplete' | 'aria-controls' | 'aria-activedescendant';\n\n/** Mentions 除 toOption 外的属性;未声明的属性落到 textarea 上(同 Textarea) */\nexport interface MentionsBaseProps<T>\n extends Omit<TextareaProps, MentionsManaged>, OpenChangeProps {\n /** 可以提及的人(ListboxOption、字符串,或任意数据加 toOption);有 load 时是第一次结果之前的选项 */\n items?: readonly T[];\n /** 远程搜索:按 @ 后面的文字调用,停下 300 ms 后请求,只采用最后一次 */\n load?: OptionsLoader<T>;\n /** 文字里提到的选项的值:父级写 _mentions={x},随文字更新 */\n _mentions?: string[];\n /** 触发的字符,默认 '@' */\n prefix?: string;\n /** 过滤:默认按显示文字包含 @ 后面的文字;false 不过滤(由调用方换 items) */\n filter?: false | ((item: T, query: string) => boolean);\n /** 列表是否打开:父级可以写 _open={x} */\n _open?: boolean;\n /** 正在取选项 */\n loading?: boolean;\n /** 自定义选项内容:`{(item, { active }) => …}`(头像、部门) */\n children?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 没有匹配的人时列表里显示的内容 */\n empty?: VNodeChild;\n /** 弹出层的部件属性 */\n propsPopover?: PartProps<'div', 'id' | 'popover'>;\n /** 列表的部件属性 */\n propsListbox?: Partial<ListboxViewProps<T>>;\n}\n\n/** 提及输入框的属性;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type MentionsProps<T = ListboxOption> = MentionsBaseProps<T> & OptionMapping<T>;\n\n/** 拆开的一段文字:普通文字,或提及(带它的选项) */\nexport interface MentionSegment {\n /** 这一段的文字(提及时含 @) */\n text: string;\n /** 提及的选项;普通文字没有 */\n mention?: ListboxOption;\n}\n\n/** @ 前面是这些字符时不算提及(邮箱 name@example.com) */\nconst WORD_BEFORE = /[A-Za-z0-9_.]/;\n\n/**\n * 把文字拆成普通文字与提及:“@名字”里的名字是 options 里某一项的显示文字(有多个能匹配时取最长的)。\n * 提及的显示(MentionText)与找出提到了谁(mentionedValues)都用它\n */\nexport function mentionSegments(\n text: string,\n options: readonly ListboxOption[],\n prefix = '@',\n): MentionSegment[] {\n const byLength = options\n .filter((option) => option.label)\n .toSorted((a, b) => b.label.length - a.label.length);\n const segments: MentionSegment[] = [];\n let plain = '';\n let index = 0;\n while (index < text.length) {\n const mention =\n text.startsWith(prefix, index) && !WORD_BEFORE.test(text[index - 1] ?? '')\n ? byLength.find((option) => text.startsWith(option.label, index + prefix.length))\n : undefined;\n if (!mention) {\n plain += text[index];\n index += 1;\n continue;\n }\n if (plain) segments.push({ text: plain });\n plain = '';\n const length = prefix.length + mention.label.length;\n segments.push({ text: text.slice(index, index + length), mention });\n index += length;\n }\n if (plain) segments.push({ text: plain });\n return segments;\n}\n\n/** 文字里提到了哪些选项(值,按第一次出现的顺序,不重复) */\nexport function mentionedValues(\n text: string,\n options: readonly ListboxOption[],\n prefix = '@',\n): string[] {\n const values = mentionSegments(text, options, prefix).flatMap((segment) =>\n segment.mention ? [segment.mention.value] : [],\n );\n return [...new Set(values)];\n}\n\n/** MentionText 与 Mentions 的样式:mention、text、panel */\nexport const mentionStyles = css.create({\n // 提及:强调色的文字,浅底\n mention: {\n color: '_accentText',\n backgroundColor: '_accentSoft',\n borderRadius: '_sm',\n paddingInline: '0.125em',\n fontWeight: '_medium',\n },\n text: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' },\n // 弹层贴着光标、不跟文本框同宽:给个合适的宽度\n panel: { width: '16em' },\n});\n\n/** 未声明的属性落到 span 上 */\nexport interface MentionTextProps extends PartProps<'span'> {\n /** 文字 */\n text: string;\n /** 能被提及的选项:“@显示文字”高亮 */\n options: readonly ListboxOption[];\n /** 触发的字符,默认 '@' */\n prefix?: string;\n /** 提及的部件属性,可以按选项计算(做成链接、悬停卡片时用 renderMention) */\n propsMention?: PartProps<'span'> | ((mention: ListboxOption) => PartProps<'span'>);\n /** 自己渲染一个提及(链接到个人页、HoverCard);不写时是高亮的文字 */\n renderMention?: (mention: ListboxOption, text: string) => VNodeChild;\n}\n\n/** 带提及的文字:“@名字”高亮,换行照常(pre-wrap) */\nexport const MentionText = _component(\n ({ text, options, prefix = '@', propsMention, renderMention, ...rest }: MentionTextProps) =>\n () => (\n <span {...mergeAttrs<'span'>({ sx: mentionStyles.text }, rest)}>\n {mentionSegments(text, options, prefix).map((segment) => {\n if (!segment.mention) return segment.text;\n if (renderMention) return renderMention(segment.mention, segment.text);\n const own =\n typeof propsMention === 'function' ? propsMention(segment.mention) : propsMention;\n return (\n <span {...mergeAttrs<'span'>({ sx: mentionStyles.mention }, own)}>{segment.text}</span>\n );\n })}\n </span>\n ),\n);\n\n/** 提及输入框:多行文本框 + 跟着 @ 弹出的人员列表 */\nexport const Mentions = _component(\n <T,>({\n _value: value = _bindable(''),\n _mentions: mentions = _bindable<string[]>([]),\n items = [],\n toOption,\n load,\n prefix = '@',\n filter,\n _open: open = _bindable(false),\n onOpenChange,\n loading = false,\n children,\n empty,\n propsRoot,\n propsPopover,\n propsListbox,\n ...rest\n }: MentionsProps<T>) => {\n const lang = useLang();\n const textMatcher = useTextMatcher();\n const listId = useId();\n let shell = _state.raw<HTMLElement | null>(null);\n let panel = _state.raw<HTMLElement | null>(null);\n // 正在提及:@ 在文字里的位置,与 @ 到光标之间的文字(过滤词)\n let trigger = _state<number | null>(null);\n let query = _state('');\n // 按 Esc 关掉的那个 @:光标还在它后面时不再弹出\n let dismissed: number | null = null;\n // 见过的选项(远程搜索换掉 items 之后,已提及的人仍能认出来)\n const known = new Map<string, ListboxOption>();\n const setOpen = openSetter(\n () => open,\n (next) => (open = next),\n () => onOpenChange,\n );\n\n const optionOf = (item: T) => toListboxOption(item, toOption);\n const textarea = () => shell?.querySelector('textarea') ?? null;\n\n const remote = useRemoteItems({\n load: () => load,\n query: () => query,\n open: () => open,\n items: () => items,\n });\n const list = useListState({\n items: () =>\n load || filter === false\n ? remote.items()\n : remote\n .items()\n .filter((item) =>\n !query\n ? true\n : filter\n ? filter(item, query)\n : textMatcher()(optionOf(item).label, query),\n ),\n toOption: optionOf,\n selected: () => [],\n mode: () => 'single',\n onChange: () => {},\n });\n useResultAnnouncement(() => ({\n query: open ? query || prefix : '',\n count: list.options.length,\n }));\n\n // 文字里提到的人:随文字与见过的选项更新\n watch(\n () => [value, remote.items()] as const,\n ([text, current]) => {\n for (const item of current) {\n const option = optionOf(item);\n known.set(option.value, option);\n }\n const next = mentionedValues(text, [...known.values()], prefix);\n if (next.join('\\n') !== mentions.join('\\n')) mentions = next;\n },\n { immediate: true },\n );\n\n // 远程结果后到:打开着又没有高亮项时高亮第一项(Enter 才能直接选中)\n watch(\n () => list.options.length,\n () => {\n if (open && list.active === undefined) list.move('first');\n },\n );\n\n /** 看光标前面是不是正在写的提及:是就按 @ 后面的文字过滤并打开,不是就关闭 */\n function detect(event?: Event) {\n const element = textarea();\n if (!element || rest.readonly || rest.disabled) return;\n const caret = element.selectionStart;\n const before = element.value.slice(0, caret);\n const at = before.lastIndexOf(prefix);\n const typed = at < 0 ? '' : before.slice(at + prefix.length);\n const mentioning =\n at >= 0 &&\n element.selectionEnd === caret &&\n !WORD_BEFORE.test(before[at - 1] ?? '') &&\n !/\\s/.test(typed) &&\n typed.length <= 30;\n if (!mentioning || at === dismissed) {\n if (!mentioning) dismissed = null;\n trigger = null;\n if (open) setOpen(false, 'focus', event);\n return;\n }\n const opening = trigger !== at || !open;\n trigger = at;\n query = typed;\n if (opening) setOpen(true, 'trigger', event);\n list.move('first');\n }\n\n /** 选中一项:把“@过滤词”换成“@名字 ”,光标放在后面 */\n function pick(choice: string, event?: Event) {\n const element = textarea();\n const option = list.options.find((entry) => entry.value === choice);\n if (!element || trigger === null || !option) return;\n known.set(option.value, option);\n const inserted = `${prefix}${option.label} `;\n const caret = element.selectionStart;\n const start = trigger;\n value = element.value.slice(0, start) + inserted + element.value.slice(caret);\n trigger = null;\n setOpen(false, 'select', event);\n void nextTick(() => {\n element.focus();\n element.setSelectionRange(start + inserted.length, start + inserted.length);\n });\n }\n\n function onKeydown(event: KeyboardEvent) {\n if (!open || isComposingKey(event)) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n dismissed = trigger;\n setOpen(false, 'escape', event);\n return;\n }\n if (event.key === 'Tab' && list.active !== undefined) {\n event.preventDefault();\n pick(list.active, event);\n return;\n }\n if (handleListKeydown(list, event, { homeEnd: false }, (choice) => pick(choice, event)))\n event.preventDefault();\n }\n\n // 移动光标(方向键、Home / End)后重新判断;↑↓ 在列表打开时是移动高亮,不算\n function onKeyup(event: KeyboardEvent) {\n if (event.isComposing) return;\n if (open && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) return;\n if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key) || !open) detect(event);\n }\n\n const { onToggle } = useAnchoredPopover(\n () => {\n const element = textarea();\n return {\n open,\n anchor: element && caretAnchor(element, () => trigger ?? element.selectionStart),\n panel,\n floating: { placement: 'bottom-start', fitHeight: true },\n restoreFocus: false,\n };\n },\n {\n close: (reason, event) => {\n if (reason === 'escape') dismissed = trigger;\n setOpen(false, reason, event);\n },\n },\n );\n\n return () => (\n <>\n <Textarea\n {...mergePartProps<TextareaProps>(\n { propsRoot: mergePartProps<PartProps<'div'>>({}, propsRoot, { ref: _toRef(shell) }) },\n rest,\n {\n 'aria-autocomplete': 'list',\n 'aria-controls': open ? listId : undefined,\n 'aria-activedescendant': open ? activeOptionId(list, listId) : undefined,\n onInput: (event: Event) => !(event as InputEvent).isComposing && detect(event),\n onCompositionend: (event: CompositionEvent) => detect(event),\n onClick: (event: MouseEvent) => detect(event),\n onKeydown,\n onKeyup,\n onBlur: (event: FocusEvent) => open && setOpen(false, 'focus', event),\n },\n )}\n _value={value}\n />\n <ListPopover\n panelRef={_toRef(panel)}\n onToggle={onToggle}\n size={rest.size}\n label={lang.mentionsList}\n loading={loading || remote.loading()}\n onPick={(choice) => pick(choice)}\n listbox={{\n list,\n id: listId,\n renderOption: children,\n empty: remote.failure() ?? empty ?? lang.mentionsEmpty,\n }}\n propsListbox={propsListbox}\n propsPopover={mergePartProps<PartProps<'div', 'id' | 'popover'>>(\n { sx: mentionStyles.panel },\n propsPopover,\n )}\n />\n </>\n );\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Mentions.js","names":["_vtp_readonlyProp","_vtp_restProps","_createVNode","_vtp_bindable","_vtp_childrenProp","_vue_shallowRef","_vue_ref","_Fragment","_mergeProps"],"sources":["../../src/input/Mentions.tsx"],"sourcesContent":["/**\n * 提及:多行文本框里输入 @ 弹出人员(或岗位、群组)列表,选中后写成“@名字 ”。留言、审批意见、协商回复里通知相关的人。\n *\n * <Field label=\"留言\"><Mentions _value={text} items={people} _mentions={notified} /></Field>\n * <Mentions _value={text} load={(query, signal) => api.searchUsers(query, { signal })} toOption={…} />\n *\n * 值仍是文字;mentions 是文字里提到的选项的值(工号、岗位编号),随文字增删——删掉“@名字”就不再算提及。\n * 显示提及过的文字用 MentionText(提及高亮),拆分用 mentionSegments。\n *\n * - 弹出:@ 在开头或前面不是字母数字(邮箱里的 @ 不算),@ 与光标之间没有空白;列表贴在 @ 的位置(caretAnchor)。\n * - 过滤:本地按显示文字包含 @ 后面的文字;给 load 时按它远程搜索(同 Combobox)。\n * - 键盘:↑↓ 移动、Enter 或 Tab 选中、Esc 关闭(这个 @ 不再弹出,直到重新输入);焦点一直在文本框里,\n * 高亮项经 aria-activedescendant 告诉读屏,结果数量播报。中文输入法组合期间不弹出、不过滤。\n *\n * 外壳与自动增高同 Textarea。部件:Root(外壳)、Textarea、Popover、Listbox。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { nextTick, useId, watch, type VNodeChild } from 'vue';\nimport { useLang, useTextMatcher } from '../config/context';\nimport { caretAnchor } from '../utils/dom/caret';\nimport { useAnchoredPopover } from '../utils/dom/floating';\nimport { isComposingKey } from '../utils/dom/hotkeys';\nimport { openSetter, type OpenChangeProps } from '../utils/dom/open-change';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { activeOptionId, type ListboxViewProps, type OptionRenderState } from './Listbox';\nimport { ListPopover } from './ListPopover';\nimport {\n handleListKeydown,\n toListboxOption,\n useListState,\n useResultAnnouncement,\n type ListboxOption,\n type OptionMapping,\n} from './list-state';\nimport { useRemoteItems, type OptionsLoader } from './remote-items';\nimport { Textarea, type TextareaProps } from './Textarea';\n\n/** 提及的弹出列表由组件管理 */\nexport type MentionsManaged = 'aria-autocomplete' | 'aria-controls' | 'aria-activedescendant';\n\n/** Mentions 除 toOption 外的属性;未声明的属性落到 textarea 上(同 Textarea) */\nexport interface MentionsBaseProps<T>\n extends Omit<TextareaProps, MentionsManaged>, OpenChangeProps {\n /** 可以提及的人(ListboxOption、字符串,或任意数据加 toOption);有 load 时是第一次结果之前的选项 */\n items?: readonly T[];\n /** 远程搜索:按 @ 后面的文字调用,停下 300 ms 后请求,只采用最后一次 */\n load?: OptionsLoader<T>;\n /** 文字里提到的选项的值:父级写 _mentions={x},随文字更新 */\n _mentions?: string[];\n /** 触发的字符,默认 '@' */\n prefix?: string;\n /** 过滤:默认按显示文字包含 @ 后面的文字;false 不过滤(由调用方换 items) */\n filter?: false | ((item: T, query: string) => boolean);\n /** 列表是否打开:父级可以写 _open={x} */\n _open?: boolean;\n /** 正在取选项 */\n loading?: boolean;\n /** 自定义选项内容:`{(item, { active }) => …}`(头像、部门) */\n children?: (item: T, state: OptionRenderState) => VNodeChild;\n /** 没有匹配的人时列表里显示的内容 */\n empty?: VNodeChild;\n /** 弹出层的部件属性 */\n propsPopover?: PartProps<'div', 'id' | 'popover'>;\n /** 列表的部件属性 */\n propsListbox?: Partial<ListboxViewProps<T>>;\n}\n\n/** 提及输入框的属性;items 不是 ListboxOption 或字符串时要给 toOption */\nexport type MentionsProps<T = ListboxOption> = MentionsBaseProps<T> & OptionMapping<T>;\n\n/** 拆开的一段文字:普通文字,或提及(带它的选项) */\nexport interface MentionSegment {\n /** 这一段的文字(提及时含 @) */\n text: string;\n /** 提及的选项;普通文字没有 */\n mention?: ListboxOption;\n}\n\n/** @ 前面是这些字符时不算提及(邮箱 name@example.com) */\nconst WORD_BEFORE = /[A-Za-z0-9_.]/;\n\n/**\n * 把文字拆成普通文字与提及:“@名字”里的名字是 options 里某一项的显示文字(有多个能匹配时取最长的)。\n * 提及的显示(MentionText)与找出提到了谁(mentionedValues)都用它\n */\nexport function mentionSegments(\n text: string,\n options: readonly ListboxOption[],\n prefix = '@',\n): MentionSegment[] {\n const byLength = options\n .filter((option) => option.label)\n .toSorted((a, b) => b.label.length - a.label.length);\n const segments: MentionSegment[] = [];\n let plain = '';\n let index = 0;\n while (index < text.length) {\n const mention =\n text.startsWith(prefix, index) && !WORD_BEFORE.test(text[index - 1] ?? '')\n ? byLength.find((option) => text.startsWith(option.label, index + prefix.length))\n : undefined;\n if (!mention) {\n plain += text[index];\n index += 1;\n continue;\n }\n if (plain) segments.push({ text: plain });\n plain = '';\n const length = prefix.length + mention.label.length;\n segments.push({ text: text.slice(index, index + length), mention });\n index += length;\n }\n if (plain) segments.push({ text: plain });\n return segments;\n}\n\n/** 文字里提到了哪些选项(值,按第一次出现的顺序,不重复) */\nexport function mentionedValues(\n text: string,\n options: readonly ListboxOption[],\n prefix = '@',\n): string[] {\n const values = mentionSegments(text, options, prefix).flatMap((segment) =>\n segment.mention ? [segment.mention.value] : [],\n );\n return [...new Set(values)];\n}\n\n/** MentionText 与 Mentions 的样式:mention、text、panel */\nexport const mentionStyles = css.create({\n // 提及:强调色的文字,浅底\n mention: {\n color: '_accentText',\n backgroundColor: '_accentSoft',\n borderRadius: '_sm',\n paddingInline: '0.125em',\n fontWeight: '_medium',\n },\n text: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' },\n // 弹层贴着光标、不跟文本框同宽:给个合适的宽度\n panel: { width: '16em' },\n});\n\n/** 未声明的属性落到 span 上 */\nexport interface MentionTextProps extends PartProps<'span'> {\n /** 文字 */\n text: string;\n /** 能被提及的选项:“@显示文字”高亮 */\n options: readonly ListboxOption[];\n /** 触发的字符,默认 '@' */\n prefix?: string;\n /** 提及的部件属性,可以按选项计算(做成链接、悬停卡片时用 renderMention) */\n propsMention?: PartProps<'span'> | ((mention: ListboxOption) => PartProps<'span'>);\n /** 自己渲染一个提及(链接到个人页、HoverCard);不写时是高亮的文字 */\n renderMention?: (mention: ListboxOption, text: string) => VNodeChild;\n}\n\n/** 带提及的文字:“@名字”高亮,换行照常(pre-wrap) */\nexport const MentionText = _component(\n ({ text, options, prefix = '@', propsMention, renderMention, ...rest }: MentionTextProps) =>\n () => (\n <span {...mergeAttrs<'span'>({ sx: mentionStyles.text }, rest)}>\n {mentionSegments(text, options, prefix).map((segment) => {\n if (!segment.mention) return segment.text;\n if (renderMention) return renderMention(segment.mention, segment.text);\n const own =\n typeof propsMention === 'function' ? propsMention(segment.mention) : propsMention;\n return (\n <span {...mergeAttrs<'span'>({ sx: mentionStyles.mention }, own)}>{segment.text}</span>\n );\n })}\n </span>\n ),\n);\n\n/** 提及输入框:多行文本框 + 跟着 @ 弹出的人员列表 */\nexport const Mentions = _component(\n <T,>({\n _value: value = _bindable(''),\n _mentions: mentions = _bindable<string[]>([]),\n items = [],\n toOption,\n load,\n prefix = '@',\n filter,\n _open: open = _bindable(false),\n onOpenChange,\n loading = false,\n children,\n empty,\n propsRoot,\n propsPopover,\n propsListbox,\n ...rest\n }: MentionsProps<T>) => {\n const lang = useLang();\n const textMatcher = useTextMatcher();\n const listId = useId();\n let shell = _state.raw<HTMLElement | null>(null);\n let panel = _state.raw<HTMLElement | null>(null);\n // 正在提及:@ 在文字里的位置,与 @ 到光标之间的文字(过滤词)\n let trigger = _state<number | null>(null);\n let query = _state('');\n // 按 Esc 关掉的那个 @:光标还在它后面时不再弹出\n let dismissed: number | null = null;\n // 见过的选项(远程搜索换掉 items 之后,已提及的人仍能认出来)\n const known = new Map<string, ListboxOption>();\n const setOpen = openSetter(\n () => open,\n (next) => (open = next),\n () => onOpenChange,\n );\n\n const optionOf = (item: T) => toListboxOption(item, toOption);\n const textarea = () => shell?.querySelector('textarea') ?? null;\n\n const remote = useRemoteItems({\n load: () => load,\n query: () => query,\n open: () => open,\n items: () => items,\n });\n const list = useListState({\n items: () =>\n load || filter === false\n ? remote.items()\n : remote\n .items()\n .filter((item) =>\n !query\n ? true\n : filter\n ? filter(item, query)\n : textMatcher()(optionOf(item).label, query),\n ),\n toOption: optionOf,\n selected: () => [],\n mode: () => 'single',\n onChange: () => {},\n });\n useResultAnnouncement(() => ({\n query: open ? query || prefix : '',\n count: list.options.length,\n }));\n\n // 文字里提到的人:随文字与见过的选项更新\n watch(\n () => [value, remote.items()] as const,\n ([text, current]) => {\n for (const item of current) {\n const option = optionOf(item);\n known.set(option.value, option);\n }\n const next = mentionedValues(text, [...known.values()], prefix);\n if (next.join('\\n') !== mentions.join('\\n')) mentions = next;\n },\n { immediate: true },\n );\n\n // 远程结果后到:打开着又没有高亮项时高亮第一项(Enter 才能直接选中)\n watch(\n () => list.options.length,\n () => {\n if (open && list.active === undefined) list.move('first');\n },\n );\n\n /** 看光标前面是不是正在写的提及:是就按 @ 后面的文字过滤并打开,不是就关闭 */\n function detect(event?: Event) {\n const element = textarea();\n if (!element || rest.readonly || rest.disabled) return;\n const caret = element.selectionStart;\n const before = element.value.slice(0, caret);\n const at = before.lastIndexOf(prefix);\n const typed = at < 0 ? '' : before.slice(at + prefix.length);\n const mentioning =\n at >= 0 &&\n element.selectionEnd === caret &&\n !WORD_BEFORE.test(before[at - 1] ?? '') &&\n !/\\s/.test(typed) &&\n typed.length <= 30;\n if (!mentioning || at === dismissed) {\n if (!mentioning) dismissed = null;\n trigger = null;\n if (open) setOpen(false, 'focus', event);\n return;\n }\n const opening = trigger !== at || !open;\n trigger = at;\n query = typed;\n if (opening) setOpen(true, 'trigger', event);\n list.move('first');\n }\n\n /** 选中一项:把“@过滤词”换成“@名字 ”,光标放在后面 */\n function pick(choice: string, event?: Event) {\n const element = textarea();\n const option = list.options.find((entry) => entry.value === choice);\n if (!element || trigger === null || !option) return;\n known.set(option.value, option);\n const inserted = `${prefix}${option.label} `;\n const caret = element.selectionStart;\n const start = trigger;\n value = element.value.slice(0, start) + inserted + element.value.slice(caret);\n trigger = null;\n setOpen(false, 'select', event);\n void nextTick(() => {\n element.focus();\n element.setSelectionRange(start + inserted.length, start + inserted.length);\n });\n }\n\n function onKeydown(event: KeyboardEvent) {\n if (!open || isComposingKey(event)) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n dismissed = trigger;\n setOpen(false, 'escape', event);\n return;\n }\n if (event.key === 'Tab' && list.active !== undefined) {\n event.preventDefault();\n pick(list.active, event);\n return;\n }\n if (handleListKeydown(list, event, { homeEnd: false }, (choice) => pick(choice, event)))\n event.preventDefault();\n }\n\n // 移动光标(方向键、Home / End)后重新判断;↑↓ 在列表打开时是移动高亮,不算\n function onKeyup(event: KeyboardEvent) {\n if (event.isComposing) return;\n if (open && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) return;\n if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key) || !open) detect(event);\n }\n\n const { onToggle } = useAnchoredPopover(\n () => {\n const element = textarea();\n return {\n open,\n anchor: element && caretAnchor(element, () => trigger ?? element.selectionStart),\n panel,\n floating: { placement: 'bottom-start', fitHeight: true },\n restoreFocus: false,\n };\n },\n {\n close: (reason, event) => {\n if (reason === 'escape') dismissed = trigger;\n setOpen(false, reason, event);\n },\n },\n );\n\n return () => (\n <>\n <Textarea\n {...mergePartProps<TextareaProps>(\n { propsRoot: mergePartProps<PartProps<'div'>>({}, propsRoot, { ref: _toRef(shell) }) },\n rest,\n {\n 'aria-autocomplete': 'list',\n 'aria-controls': open ? listId : undefined,\n 'aria-activedescendant': open ? activeOptionId(list, listId) : undefined,\n onInput: (event: Event) => !(event as InputEvent).isComposing && detect(event),\n onCompositionend: (event: CompositionEvent) => detect(event),\n onClick: (event: MouseEvent) => detect(event),\n onKeydown,\n onKeyup,\n onBlur: (event: FocusEvent) => open && setOpen(false, 'focus', event),\n },\n )}\n _value={value}\n />\n <ListPopover\n panelRef={_toRef(panel)}\n onToggle={onToggle}\n size={rest.size}\n label={lang.mentionsList}\n loading={loading || remote.loading()}\n onPick={(choice) => pick(choice)}\n listbox={{\n list,\n id: listId,\n renderOption: children,\n empty: remote.failure() ?? empty ?? lang.mentionsEmpty,\n }}\n propsListbox={propsListbox}\n propsPopover={mergePartProps<PartProps<'div', 'id' | 'popover'>>(\n { sx: mentionStyles.panel },\n propsPopover,\n )}\n />\n </>\n );\n },\n);\n"],"mappings":"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"}
|
|
@@ -7,8 +7,8 @@ import { useFieldControl } from "../form/Field.js";
|
|
|
7
7
|
import { controlAffix, controlClear, controlIcon, controlParts, useControlAppearance } from "./control.js";
|
|
8
8
|
import { FormValue } from "../form/FormValue.js";
|
|
9
9
|
import { normalizeNumber } from "../utils/data/number-parse.js";
|
|
10
|
-
import { Fragment, createVNode,
|
|
11
|
-
import { _component, bindable, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
10
|
+
import { Fragment, createVNode, onBeforeUnmount, ref, shallowRef, useId } from "vue";
|
|
11
|
+
import { _component, bindable, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
12
12
|
import { Minus, Plus } from "@lucide/icons";
|
|
13
13
|
//#region src/input/NumberField.tsx
|
|
14
14
|
/**
|
|
@@ -186,7 +186,7 @@ var NumberField = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
186
186
|
const integer = isInteger(increment()) && (lowest == null || isInteger(lowest));
|
|
187
187
|
const atLowest = value.value != null && lowest != null && compareNumeric(value.value, lowest) <= 0;
|
|
188
188
|
const atHighest = value.value != null && highest != null && compareNumeric(value.value, highest) >= 0;
|
|
189
|
-
return createVNode("div",
|
|
189
|
+
return createVNode("div", mergeJsxProps(mergeAttrs(s.root, propsRoot.value), {
|
|
190
190
|
"data-invalid": state.invalid || void 0,
|
|
191
191
|
"data-disabled": state.disabled || void 0
|
|
192
192
|
}), [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NumberField.js","names":["_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_vue_shallowRef","_vue_ref","_createVNode","_mergeProps","_Fragment"],"sources":["../../src/input/NumberField.tsx"],"sourcesContent":["/**\n * 数字输入框(role=\"spinbutton\"):外壳同 TextField,右侧带减少 / 增加按钮。参考 React Aria NumberField、WAI-ARIA Spinbutton。\n *\n * <Field label=\"数量\"><NumberField _value={count} min={1} max={99} /></Field>\n * <NumberField _value={price} step={0.01} format={{ style: 'currency', currency: 'CNY' }} aria-label=\"单价\" />\n * <NumberField _value={amount} valueType={Decimal} step={new Decimal('0.01')} /> // 金额:Decimal 精确运算\n * <NumberField _value={yuan} format={{ scale: 10_000, maximumFractionDigits: 2 }} suffix=\"万元\" /> // 值是元,按万元输入\n *\n * - 值可以是 number、bigint 或 decimal.js 的 Decimal,写回时保持传入的类型(一开始为空时按 valueType);\n * bigint 与 Decimal 的加减、对齐步长都精确运算,不经过浮点数。表单提交的是十进制文本(FormValue),不是显示的格式。\n * - 输入时是普通文本;失焦或回车时解析、夹到 min–max、对齐到 step 后写回(snapToStep,没有浮点误差),解析不了时恢复原值;\n * 清空后写回 null。没在编辑时按 format(NumericFormat)与当前语言显示,编辑时去掉千分位。\n * format 写了 scale(10 的整数次幂)时,显示与编辑都按缩放后的单位(值是元、输入的是万元),写回时精确移位回去;\n * min、max、step 仍按值本身的单位。单位文字用 prefix、suffix,放在输入框里、文字的前后。\n * - 认当前语言的小数点与千分位,也认中文输入法打出的全角数字和全角句点。\n * - ↑ / ↓ 加减 step,Page Up / Page Down 加减 bigStep,Home / End 到 min / max;按钮不在 Tab 顺序里(键盘用方向键),\n * 按住按钮连续加减(先停 400ms,之后每 60ms 一步,同 React Aria)。\n *\n * 未声明的属性落到 input(主元素)上,外壳用 propsRoot。部件:Root、Prefix、Input、Suffix、Decrement、Increment(IconButton)。\n */\nimport { Minus, Plus } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport type { PresetIconButtonProps } from '../base/CloseButton';\nimport { onBeforeUnmount, useId, type VNodeChild } from 'vue';\nimport { IconButton, type IconButtonProps } from '../base/IconButton';\nimport { useLang } from '../config/context';\nimport { useFormat, type NumericFormat } from '../config/lang/format';\nimport { useFieldControl } from '../form/Field';\nimport { FormValue } from '../form/FormValue';\nimport { normalizeNumber } from '../utils/data/number-parse';\nimport {\n addNumeric,\n compareNumeric,\n multiplyNumeric,\n numericKind,\n shiftDecimal,\n snapNumeric,\n toNumber,\n toNumericKind,\n type NumericKind,\n type NumericValue,\n} from '../utils/data/numeric';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport {\n controlAffix,\n controlClear,\n controlIcon,\n controlParts,\n useControlAppearance,\n type ControlIconOptions,\n type ControlOptions,\n} from './control';\n\n/** 值、角色与状态由组件管理 */\nexport type NumberFieldManaged =\n | 'value'\n | 'name'\n | 'type'\n | 'role'\n | 'inputmode'\n | 'min'\n | 'max'\n | 'step'\n | 'size'\n | 'prefix'\n | 'disabled'\n | 'readonly'\n | 'required'\n | 'aria-valuenow'\n | 'aria-valuemin'\n | 'aria-valuemax'\n | 'aria-valuetext'\n | 'aria-invalid'\n | 'aria-describedby';\n\n/** 未声明的属性落到 input(主元素)上;外壳用 propsRoot */\nexport interface NumberFieldProps\n extends PartProps<'input', NumberFieldManaged>, ControlOptions, ControlIconOptions {\n /** 当前值:number、bigint 或 Decimal,父级写 _value={x};空着时为 null。写回时保持传入的类型 */\n _value?: NumericValue | null;\n /**\n * 值一开始为空时写回的类型:'number'(默认)、'bigint',或 Decimal 构造函数(`valueType={Decimal}`,\n * Decimal.clone 配过精度的也行)。有值以后按值本身的类型\n */\n valueType?: NumericKind;\n /** 最小值;在表单里默认按 schema(z.number().min(0)、decimalSchema 的 min) */\n min?: NumericValue;\n /** 最大值;在表单里默认按 schema */\n max?: NumericValue;\n /** 步长,默认 1(schema 是 z.number().multipleOf(0.5) 时按它);提交时对齐到 min + n × step。bigint 至少为 1 */\n step?: NumericValue;\n /** Page Up / Page Down 的步长,默认 step × 10 */\n bigStep?: NumericValue;\n /** 表单字段名:提交的是十进制文本(不带千分位与货币符号),不是输入框里显示的格式 */\n name?: string;\n /**\n * 显示格式,如 `{ style: 'currency', currency: 'CNY' }`;写了 scale 时按缩放后的单位显示与输入:\n * 值是元、按万元输入写 `{ scale: 10_000, maximumFractionDigits: 2 }` 并配 `suffix=\"万元\"`\n */\n format?: NumericFormat;\n /** 输入框里文字前的内容(币种符号、图标) */\n prefix?: VNodeChild;\n /** 输入框里文字后的内容:单位,如“万元”“元/件” */\n suffix?: VNodeChild;\n /** 显示减少 / 增加按钮,默认 true;只读时不显示 */\n stepper?: boolean;\n /** 有值时显示清空按钮:清空后值是 null(选填的数量、金额) */\n clearable?: boolean;\n /** 不可用 */\n disabled?: boolean;\n /** 只读:能聚焦、选中复制,不能改;不显示加减按钮 */\n readonly?: boolean;\n /** 必填 */\n required?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 外壳的部件属性 */\n propsRoot?: PartProps<'div'>;\n /** 前缀的部件属性(指向输入框的 label) */\n propsPrefix?: PartProps<'label'>;\n /** 后缀(单位)的部件属性(指向输入框的 label) */\n propsSuffix?: PartProps<'label'>;\n /** 减少按钮的部件属性 */\n propsDecrement?: Partial<IconButtonProps>;\n /** 增加按钮的部件属性 */\n propsIncrement?: Partial<IconButtonProps>;\n /** 清空按钮的部件属性 */\n propsClear?: Partial<PresetIconButtonProps>;\n}\n\n/** 数字输入框:输入、方向键与按钮加减,提交时夹到范围并对齐步长 */\nexport const NumberField = _component(\n ({\n _value: value = _bindable<NumericValue | null>(null),\n valueType = 'number',\n min,\n max,\n step,\n bigStep,\n format,\n prefix,\n suffix,\n name,\n stepper = true,\n clearable = false,\n variant,\n size,\n radius,\n disabled,\n readonly,\n required,\n invalid,\n id,\n propsRoot,\n propsPrefix,\n propsSuffix,\n icon,\n propsIcon,\n propsDecrement,\n propsIncrement,\n propsClear,\n ...rest\n }: NumberFieldProps) => {\n const lang = useLang();\n const formatter = useFormat();\n const appearance = useControlAppearance(() => ({ variant, size, radius }));\n const control = useFieldControl(() => ({ id, invalid, required, disabled, readonly }));\n // 前后缀的 label 要指向输入框:不在 Field 里、也没写 id 时用自己生成的\n const ownId = useId();\n // 范围与步长:显式写的 > 表单 schema 推出的(z.number().int().min(0))> 不限、1\n const low = (): NumericValue | undefined => min ?? control().constraints.min;\n const high = (): NumericValue | undefined => max ?? control().constraints.max;\n const increment = (): NumericValue => step ?? control().constraints.step ?? 1;\n let input = _state.raw<HTMLInputElement | null>(null);\n // 编辑中的文字;不在编辑时为 null,显示格式化后的值\n let draft = _state<string | null>(null);\n // 写回的类型:最近一次有值时的类型;一直为空时按 valueType\n let lastKind: NumericKind | undefined;\n const kind = () => {\n if (value != null) lastKind = numericKind(value);\n return lastKind ?? valueType;\n };\n\n const snap = (next: NumericValue) => snapNumeric(next, increment(), low(), high(), kind());\n /** 缩放的位数:scale 为 10_000 时是 4(输入的数小数点右移 4 位才是值) */\n const places = () => (format?.scale ? Math.round(Math.log10(format.scale)) : 0);\n /** 输入框里的十进制文本 → 值的十进制文本 */\n const unscaled = (text: string) => (places() ? shiftDecimal(text, -places()) : text);\n const plain = (next: NumericValue | null) =>\n next == null\n ? ''\n : formatter.number(next, {\n useGrouping: false,\n maximumFractionDigits: 20,\n scale: format?.scale,\n });\n\n /**\n * 解析并写回(解析不了时恢复原值)。文字没改过、或只读与不可用时不写:只是聚焦再离开,\n * 不该把值按步长取整、夹到范围里,把表单弄脏\n */\n function commit() {\n if (draft == null) return;\n const state = control();\n if (draft === plain(value) || state.disabled || state.readonly) {\n draft = null;\n return;\n }\n const parsed = normalizeNumber(draft, lang.code);\n if (parsed === null) value = null;\n else if (parsed !== undefined) value = snap(toNumericKind(unscaled(parsed), kind()));\n draft = null;\n }\n\n /** 从当前值(编辑中则先解析)加减 delta 的 times 倍;没有值时从 min(或 0)开始 */\n function stepBy(delta: NumericValue, times: number) {\n const typed = draft != null ? normalizeNumber(draft, lang.code) : null;\n const current =\n draft != null ? (typed == null ? null : toNumericKind(unscaled(typed), kind())) : value;\n const offset = multiplyNumeric(delta, times, kind());\n setValue(snap(current == null ? (low() ?? 0) : addNumeric(current, offset, kind())));\n }\n\n /** 键盘改值:编辑中的文字跟着换,失焦时才不会被旧文字写回去 */\n function setValue(next: NumericValue) {\n value = next;\n if (draft != null) draft = plain(value);\n }\n\n function onKeydown(event: KeyboardEvent) {\n const state = control();\n if (state.disabled || state.readonly) return;\n const lowest = low();\n const highest = high();\n const actions: Record<string, (() => void) | undefined> = {\n ArrowUp: () => stepBy(increment(), 1),\n ArrowDown: () => stepBy(increment(), -1),\n PageUp: () => (bigStep == null ? stepBy(increment(), 10) : stepBy(bigStep, 1)),\n PageDown: () => (bigStep == null ? stepBy(increment(), -10) : stepBy(bigStep, -1)),\n Home: lowest == null ? undefined : () => setValue(toNumericKind(lowest, kind())),\n End: highest == null ? undefined : () => setValue(toNumericKind(highest, kind())),\n Enter: commit,\n };\n const action = actions[event.key];\n if (!action) return;\n if (event.key !== 'Enter') event.preventDefault();\n action();\n }\n\n // 按住按钮不让 input 失焦:焦点留在输入框里,接着可以用方向键\n const keepFocus = (event: PointerEvent) => {\n if (document.activeElement === input) event.preventDefault();\n };\n\n // 按住步进按钮:先走一步,停一下后连续走;松开(在哪里松开都算)时停\n let repeat: ReturnType<typeof setTimeout> | undefined;\n // 指针按下已经走过一步,随后的 click 不再走(读屏、键盘触发的 click 照常走一步)\n let stepped = false;\n const stopRepeat = () => clearTimeout(repeat);\n /** direction 为 1 或 -1:按住时连续加减 step */\n function hold(event: PointerEvent, direction: number) {\n keepFocus(event);\n if (event.button !== 0) return;\n stepped = true;\n stepBy(increment(), direction);\n const tick = (wait: number) => {\n repeat = setTimeout(() => {\n stepBy(increment(), direction);\n tick(60);\n }, wait);\n };\n tick(400);\n window.addEventListener('pointerup', stopRepeat, { once: true });\n window.addEventListener('pointercancel', stopRepeat, { once: true });\n }\n function clickStep(direction: number) {\n if (stepped) stepped = false;\n else stepBy(increment(), direction);\n }\n onBeforeUnmount(stopRepeat);\n\n /** 读屏与原生属性只认 number:bigint、Decimal 转成近似的浮点数 */\n const approximate = (item: NumericValue | null | undefined) =>\n item == null ? undefined : toNumber(item);\n const isInteger = (item: NumericValue) =>\n typeof item === 'bigint' ||\n (typeof item === 'number' ? Number.isInteger(item) : item.isInteger());\n\n return () => {\n const state = control();\n const s = controlParts(appearance(), state);\n const inputId = state.id ?? ownId;\n const editable = !state.disabled && !state.readonly;\n const shown = draft ?? (value == null ? '' : formatter.number(value, format));\n const lowest = low();\n const highest = high();\n const integer = isInteger(increment()) && (lowest == null || isInteger(lowest));\n const atLowest = value != null && lowest != null && compareNumeric(value, lowest) <= 0;\n const atHighest = value != null && highest != null && compareNumeric(value, highest) >= 0;\n return (\n <div\n {...mergeAttrs<'div'>(s.root, propsRoot)}\n data-invalid={state.invalid || undefined}\n data-disabled={state.disabled || undefined}\n >\n {controlIcon(s, icon, propsIcon)}\n {controlAffix(s, prefix, inputId, propsPrefix)}\n <input\n {...mergeAttrs<'input'>(s.input, rest, {\n ref: _toRef(input),\n id: inputId,\n type: 'text',\n role: 'spinbutton',\n inputmode:\n integer && lowest != null && compareNumeric(lowest, 0) >= 0 ? 'numeric' : 'decimal',\n autocomplete: rest.autocomplete ?? 'off',\n value: shown,\n disabled: state.disabled,\n readonly: state.readonly,\n required: state.required,\n 'aria-valuenow': approximate(value),\n 'aria-valuemin': approximate(lowest),\n 'aria-valuemax': approximate(highest),\n 'aria-valuetext': value == null ? undefined : formatter.number(value, format),\n 'aria-invalid': state.invalid || undefined,\n 'aria-describedby': state.describedBy,\n onFocus: () => (draft = plain(value)),\n onInput: (event: Event) => (draft = (event.target as HTMLInputElement).value),\n onBlur: commit,\n onKeydown,\n })}\n />\n {controlAffix(s, suffix, inputId, propsSuffix)}\n {clearable &&\n editable &&\n (value != null || !!draft) &&\n controlClear(s, propsClear, () => {\n draft = null;\n value = null;\n input?.focus();\n })}\n {stepper && !state.readonly && (\n <>\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n ...s.button,\n radius: 'full',\n icon: Minus,\n 'aria-label': lang.numberDecrement,\n },\n propsDecrement,\n {\n tabindex: -1,\n disabled: !editable || atLowest,\n onPointerdown: (event: PointerEvent) => hold(event, -1),\n onClick: () => clickStep(-1),\n },\n )}\n />\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n ...s.button,\n radius: 'full',\n icon: Plus,\n 'aria-label': lang.numberIncrement,\n },\n propsIncrement,\n {\n tabindex: -1,\n disabled: !editable || atHighest,\n onPointerdown: (event: PointerEvent) => hold(event, 1),\n onClick: () => clickStep(1),\n },\n )}\n />\n </>\n )}\n <FormValue name={name} value={value} disabled={state.disabled} />\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"NumberField.js","names":["_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_vue_shallowRef","_vue_ref","_createVNode","_mergeProps","_Fragment"],"sources":["../../src/input/NumberField.tsx"],"sourcesContent":["/**\n * 数字输入框(role=\"spinbutton\"):外壳同 TextField,右侧带减少 / 增加按钮。参考 React Aria NumberField、WAI-ARIA Spinbutton。\n *\n * <Field label=\"数量\"><NumberField _value={count} min={1} max={99} /></Field>\n * <NumberField _value={price} step={0.01} format={{ style: 'currency', currency: 'CNY' }} aria-label=\"单价\" />\n * <NumberField _value={amount} valueType={Decimal} step={new Decimal('0.01')} /> // 金额:Decimal 精确运算\n * <NumberField _value={yuan} format={{ scale: 10_000, maximumFractionDigits: 2 }} suffix=\"万元\" /> // 值是元,按万元输入\n *\n * - 值可以是 number、bigint 或 decimal.js 的 Decimal,写回时保持传入的类型(一开始为空时按 valueType);\n * bigint 与 Decimal 的加减、对齐步长都精确运算,不经过浮点数。表单提交的是十进制文本(FormValue),不是显示的格式。\n * - 输入时是普通文本;失焦或回车时解析、夹到 min–max、对齐到 step 后写回(snapToStep,没有浮点误差),解析不了时恢复原值;\n * 清空后写回 null。没在编辑时按 format(NumericFormat)与当前语言显示,编辑时去掉千分位。\n * format 写了 scale(10 的整数次幂)时,显示与编辑都按缩放后的单位(值是元、输入的是万元),写回时精确移位回去;\n * min、max、step 仍按值本身的单位。单位文字用 prefix、suffix,放在输入框里、文字的前后。\n * - 认当前语言的小数点与千分位,也认中文输入法打出的全角数字和全角句点。\n * - ↑ / ↓ 加减 step,Page Up / Page Down 加减 bigStep,Home / End 到 min / max;按钮不在 Tab 顺序里(键盘用方向键),\n * 按住按钮连续加减(先停 400ms,之后每 60ms 一步,同 React Aria)。\n *\n * 未声明的属性落到 input(主元素)上,外壳用 propsRoot。部件:Root、Prefix、Input、Suffix、Decrement、Increment(IconButton)。\n */\nimport { Minus, Plus } from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport type { PresetIconButtonProps } from '../base/CloseButton';\nimport { onBeforeUnmount, useId, type VNodeChild } from 'vue';\nimport { IconButton, type IconButtonProps } from '../base/IconButton';\nimport { useLang } from '../config/context';\nimport { useFormat, type NumericFormat } from '../config/lang/format';\nimport { useFieldControl } from '../form/Field';\nimport { FormValue } from '../form/FormValue';\nimport { normalizeNumber } from '../utils/data/number-parse';\nimport {\n addNumeric,\n compareNumeric,\n multiplyNumeric,\n numericKind,\n shiftDecimal,\n snapNumeric,\n toNumber,\n toNumericKind,\n type NumericKind,\n type NumericValue,\n} from '../utils/data/numeric';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport {\n controlAffix,\n controlClear,\n controlIcon,\n controlParts,\n useControlAppearance,\n type ControlIconOptions,\n type ControlOptions,\n} from './control';\n\n/** 值、角色与状态由组件管理 */\nexport type NumberFieldManaged =\n | 'value'\n | 'name'\n | 'type'\n | 'role'\n | 'inputmode'\n | 'min'\n | 'max'\n | 'step'\n | 'size'\n | 'prefix'\n | 'disabled'\n | 'readonly'\n | 'required'\n | 'aria-valuenow'\n | 'aria-valuemin'\n | 'aria-valuemax'\n | 'aria-valuetext'\n | 'aria-invalid'\n | 'aria-describedby';\n\n/** 未声明的属性落到 input(主元素)上;外壳用 propsRoot */\nexport interface NumberFieldProps\n extends PartProps<'input', NumberFieldManaged>, ControlOptions, ControlIconOptions {\n /** 当前值:number、bigint 或 Decimal,父级写 _value={x};空着时为 null。写回时保持传入的类型 */\n _value?: NumericValue | null;\n /**\n * 值一开始为空时写回的类型:'number'(默认)、'bigint',或 Decimal 构造函数(`valueType={Decimal}`,\n * Decimal.clone 配过精度的也行)。有值以后按值本身的类型\n */\n valueType?: NumericKind;\n /** 最小值;在表单里默认按 schema(z.number().min(0)、decimalSchema 的 min) */\n min?: NumericValue;\n /** 最大值;在表单里默认按 schema */\n max?: NumericValue;\n /** 步长,默认 1(schema 是 z.number().multipleOf(0.5) 时按它);提交时对齐到 min + n × step。bigint 至少为 1 */\n step?: NumericValue;\n /** Page Up / Page Down 的步长,默认 step × 10 */\n bigStep?: NumericValue;\n /** 表单字段名:提交的是十进制文本(不带千分位与货币符号),不是输入框里显示的格式 */\n name?: string;\n /**\n * 显示格式,如 `{ style: 'currency', currency: 'CNY' }`;写了 scale 时按缩放后的单位显示与输入:\n * 值是元、按万元输入写 `{ scale: 10_000, maximumFractionDigits: 2 }` 并配 `suffix=\"万元\"`\n */\n format?: NumericFormat;\n /** 输入框里文字前的内容(币种符号、图标) */\n prefix?: VNodeChild;\n /** 输入框里文字后的内容:单位,如“万元”“元/件” */\n suffix?: VNodeChild;\n /** 显示减少 / 增加按钮,默认 true;只读时不显示 */\n stepper?: boolean;\n /** 有值时显示清空按钮:清空后值是 null(选填的数量、金额) */\n clearable?: boolean;\n /** 不可用 */\n disabled?: boolean;\n /** 只读:能聚焦、选中复制,不能改;不显示加减按钮 */\n readonly?: boolean;\n /** 必填 */\n required?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 外壳的部件属性 */\n propsRoot?: PartProps<'div'>;\n /** 前缀的部件属性(指向输入框的 label) */\n propsPrefix?: PartProps<'label'>;\n /** 后缀(单位)的部件属性(指向输入框的 label) */\n propsSuffix?: PartProps<'label'>;\n /** 减少按钮的部件属性 */\n propsDecrement?: Partial<IconButtonProps>;\n /** 增加按钮的部件属性 */\n propsIncrement?: Partial<IconButtonProps>;\n /** 清空按钮的部件属性 */\n propsClear?: Partial<PresetIconButtonProps>;\n}\n\n/** 数字输入框:输入、方向键与按钮加减,提交时夹到范围并对齐步长 */\nexport const NumberField = _component(\n ({\n _value: value = _bindable<NumericValue | null>(null),\n valueType = 'number',\n min,\n max,\n step,\n bigStep,\n format,\n prefix,\n suffix,\n name,\n stepper = true,\n clearable = false,\n variant,\n size,\n radius,\n disabled,\n readonly,\n required,\n invalid,\n id,\n propsRoot,\n propsPrefix,\n propsSuffix,\n icon,\n propsIcon,\n propsDecrement,\n propsIncrement,\n propsClear,\n ...rest\n }: NumberFieldProps) => {\n const lang = useLang();\n const formatter = useFormat();\n const appearance = useControlAppearance(() => ({ variant, size, radius }));\n const control = useFieldControl(() => ({ id, invalid, required, disabled, readonly }));\n // 前后缀的 label 要指向输入框:不在 Field 里、也没写 id 时用自己生成的\n const ownId = useId();\n // 范围与步长:显式写的 > 表单 schema 推出的(z.number().int().min(0))> 不限、1\n const low = (): NumericValue | undefined => min ?? control().constraints.min;\n const high = (): NumericValue | undefined => max ?? control().constraints.max;\n const increment = (): NumericValue => step ?? control().constraints.step ?? 1;\n let input = _state.raw<HTMLInputElement | null>(null);\n // 编辑中的文字;不在编辑时为 null,显示格式化后的值\n let draft = _state<string | null>(null);\n // 写回的类型:最近一次有值时的类型;一直为空时按 valueType\n let lastKind: NumericKind | undefined;\n const kind = () => {\n if (value != null) lastKind = numericKind(value);\n return lastKind ?? valueType;\n };\n\n const snap = (next: NumericValue) => snapNumeric(next, increment(), low(), high(), kind());\n /** 缩放的位数:scale 为 10_000 时是 4(输入的数小数点右移 4 位才是值) */\n const places = () => (format?.scale ? Math.round(Math.log10(format.scale)) : 0);\n /** 输入框里的十进制文本 → 值的十进制文本 */\n const unscaled = (text: string) => (places() ? shiftDecimal(text, -places()) : text);\n const plain = (next: NumericValue | null) =>\n next == null\n ? ''\n : formatter.number(next, {\n useGrouping: false,\n maximumFractionDigits: 20,\n scale: format?.scale,\n });\n\n /**\n * 解析并写回(解析不了时恢复原值)。文字没改过、或只读与不可用时不写:只是聚焦再离开,\n * 不该把值按步长取整、夹到范围里,把表单弄脏\n */\n function commit() {\n if (draft == null) return;\n const state = control();\n if (draft === plain(value) || state.disabled || state.readonly) {\n draft = null;\n return;\n }\n const parsed = normalizeNumber(draft, lang.code);\n if (parsed === null) value = null;\n else if (parsed !== undefined) value = snap(toNumericKind(unscaled(parsed), kind()));\n draft = null;\n }\n\n /** 从当前值(编辑中则先解析)加减 delta 的 times 倍;没有值时从 min(或 0)开始 */\n function stepBy(delta: NumericValue, times: number) {\n const typed = draft != null ? normalizeNumber(draft, lang.code) : null;\n const current =\n draft != null ? (typed == null ? null : toNumericKind(unscaled(typed), kind())) : value;\n const offset = multiplyNumeric(delta, times, kind());\n setValue(snap(current == null ? (low() ?? 0) : addNumeric(current, offset, kind())));\n }\n\n /** 键盘改值:编辑中的文字跟着换,失焦时才不会被旧文字写回去 */\n function setValue(next: NumericValue) {\n value = next;\n if (draft != null) draft = plain(value);\n }\n\n function onKeydown(event: KeyboardEvent) {\n const state = control();\n if (state.disabled || state.readonly) return;\n const lowest = low();\n const highest = high();\n const actions: Record<string, (() => void) | undefined> = {\n ArrowUp: () => stepBy(increment(), 1),\n ArrowDown: () => stepBy(increment(), -1),\n PageUp: () => (bigStep == null ? stepBy(increment(), 10) : stepBy(bigStep, 1)),\n PageDown: () => (bigStep == null ? stepBy(increment(), -10) : stepBy(bigStep, -1)),\n Home: lowest == null ? undefined : () => setValue(toNumericKind(lowest, kind())),\n End: highest == null ? undefined : () => setValue(toNumericKind(highest, kind())),\n Enter: commit,\n };\n const action = actions[event.key];\n if (!action) return;\n if (event.key !== 'Enter') event.preventDefault();\n action();\n }\n\n // 按住按钮不让 input 失焦:焦点留在输入框里,接着可以用方向键\n const keepFocus = (event: PointerEvent) => {\n if (document.activeElement === input) event.preventDefault();\n };\n\n // 按住步进按钮:先走一步,停一下后连续走;松开(在哪里松开都算)时停\n let repeat: ReturnType<typeof setTimeout> | undefined;\n // 指针按下已经走过一步,随后的 click 不再走(读屏、键盘触发的 click 照常走一步)\n let stepped = false;\n const stopRepeat = () => clearTimeout(repeat);\n /** direction 为 1 或 -1:按住时连续加减 step */\n function hold(event: PointerEvent, direction: number) {\n keepFocus(event);\n if (event.button !== 0) return;\n stepped = true;\n stepBy(increment(), direction);\n const tick = (wait: number) => {\n repeat = setTimeout(() => {\n stepBy(increment(), direction);\n tick(60);\n }, wait);\n };\n tick(400);\n window.addEventListener('pointerup', stopRepeat, { once: true });\n window.addEventListener('pointercancel', stopRepeat, { once: true });\n }\n function clickStep(direction: number) {\n if (stepped) stepped = false;\n else stepBy(increment(), direction);\n }\n onBeforeUnmount(stopRepeat);\n\n /** 读屏与原生属性只认 number:bigint、Decimal 转成近似的浮点数 */\n const approximate = (item: NumericValue | null | undefined) =>\n item == null ? undefined : toNumber(item);\n const isInteger = (item: NumericValue) =>\n typeof item === 'bigint' ||\n (typeof item === 'number' ? Number.isInteger(item) : item.isInteger());\n\n return () => {\n const state = control();\n const s = controlParts(appearance(), state);\n const inputId = state.id ?? ownId;\n const editable = !state.disabled && !state.readonly;\n const shown = draft ?? (value == null ? '' : formatter.number(value, format));\n const lowest = low();\n const highest = high();\n const integer = isInteger(increment()) && (lowest == null || isInteger(lowest));\n const atLowest = value != null && lowest != null && compareNumeric(value, lowest) <= 0;\n const atHighest = value != null && highest != null && compareNumeric(value, highest) >= 0;\n return (\n <div\n {...mergeAttrs<'div'>(s.root, propsRoot)}\n data-invalid={state.invalid || undefined}\n data-disabled={state.disabled || undefined}\n >\n {controlIcon(s, icon, propsIcon)}\n {controlAffix(s, prefix, inputId, propsPrefix)}\n <input\n {...mergeAttrs<'input'>(s.input, rest, {\n ref: _toRef(input),\n id: inputId,\n type: 'text',\n role: 'spinbutton',\n inputmode:\n integer && lowest != null && compareNumeric(lowest, 0) >= 0 ? 'numeric' : 'decimal',\n autocomplete: rest.autocomplete ?? 'off',\n value: shown,\n disabled: state.disabled,\n readonly: state.readonly,\n required: state.required,\n 'aria-valuenow': approximate(value),\n 'aria-valuemin': approximate(lowest),\n 'aria-valuemax': approximate(highest),\n 'aria-valuetext': value == null ? undefined : formatter.number(value, format),\n 'aria-invalid': state.invalid || undefined,\n 'aria-describedby': state.describedBy,\n onFocus: () => (draft = plain(value)),\n onInput: (event: Event) => (draft = (event.target as HTMLInputElement).value),\n onBlur: commit,\n onKeydown,\n })}\n />\n {controlAffix(s, suffix, inputId, propsSuffix)}\n {clearable &&\n editable &&\n (value != null || !!draft) &&\n controlClear(s, propsClear, () => {\n draft = null;\n value = null;\n input?.focus();\n })}\n {stepper && !state.readonly && (\n <>\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n ...s.button,\n radius: 'full',\n icon: Minus,\n 'aria-label': lang.numberDecrement,\n },\n propsDecrement,\n {\n tabindex: -1,\n disabled: !editable || atLowest,\n onPointerdown: (event: PointerEvent) => hold(event, -1),\n onClick: () => clickStep(-1),\n },\n )}\n />\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n ...s.button,\n radius: 'full',\n icon: Plus,\n 'aria-label': lang.numberIncrement,\n },\n propsIncrement,\n {\n tabindex: -1,\n disabled: !editable || atHighest,\n onPointerdown: (event: PointerEvent) => hold(event, 1),\n onClick: () => clickStep(1),\n },\n )}\n />\n </>\n )}\n <FormValue name={name} value={value} disabled={state.disabled} />\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
@@ -4,8 +4,8 @@ import { defaultLabelledBy, mergeAttrs, mergePartProps } from "../utils/props.js
|
|
|
4
4
|
import { fontSizeStyles } from "../utils/style/typography.js";
|
|
5
5
|
import { useFieldControl } from "../form/Field.js";
|
|
6
6
|
import { NumberField } from "./NumberField.js";
|
|
7
|
-
import { createVNode
|
|
8
|
-
import { _component, bindable, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
7
|
+
import { createVNode } from "vue";
|
|
8
|
+
import { _component, bindable, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
9
9
|
import * as css from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
10
10
|
//#region src/input/NumberRangeField.tsx
|
|
11
11
|
/** 区间的值:[起, 止],空着的一端是 null */
|
|
@@ -89,7 +89,7 @@ var NumberRangeField = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
89
89
|
stepper: false,
|
|
90
90
|
propsRoot: { sx: numberRangeFieldStyles.field }
|
|
91
91
|
};
|
|
92
|
-
return createVNode("div",
|
|
92
|
+
return createVNode("div", mergeJsxProps(mergeAttrs({ sx: [numberRangeFieldStyles.root, size.value && fontSizeStyles[size.value]] }, rest, {
|
|
93
93
|
role: "group",
|
|
94
94
|
"aria-labelledby": defaultLabelledBy(rest, state.labelledBy),
|
|
95
95
|
"aria-describedby": state.describedBy
|
|
@@ -97,7 +97,7 @@ var NumberRangeField = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
97
97
|
"data-disabled": state.disabled || void 0,
|
|
98
98
|
"data-invalid": state.invalid || void 0
|
|
99
99
|
}), [
|
|
100
|
-
createVNode(NumberField,
|
|
100
|
+
createVNode(NumberField, mergeJsxProps(mergePartProps({
|
|
101
101
|
...shared,
|
|
102
102
|
placeholder: placeholder.value?.[0] ?? lang.sliderMin
|
|
103
103
|
}, propsStart.value, {
|
|
@@ -110,7 +110,7 @@ var NumberRangeField = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
110
110
|
"onUpdate:_value": _cache[0] || (_cache[0] = (next) => set(0, next))
|
|
111
111
|
}), null),
|
|
112
112
|
createVNode("span", mergeAttrs({ sx: numberRangeFieldStyles.separator }, propsSeparator.value, { "aria-hidden": true }), [separator.value]),
|
|
113
|
-
createVNode(NumberField,
|
|
113
|
+
createVNode(NumberField, mergeJsxProps(mergePartProps({
|
|
114
114
|
...shared,
|
|
115
115
|
placeholder: placeholder.value?.[1] ?? lang.sliderMax
|
|
116
116
|
}, propsEnd.value, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NumberRangeField.js","names":["_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_createVNode","_mergeProps"],"sources":["../../src/input/NumberRangeField.tsx"],"sourcesContent":["/**\n * 数值区间(div role=\"group\"):两个 NumberField,最小到最大。预期收益量级、金额范围、数量区间的录入与筛选。\n *\n * <Field label=\"预期收益量级\">\n * <NumberRangeField _value={range} format={{ scale: 10_000 }} suffix=\"万元\" min={0} />\n * </Field>\n *\n * value 是 [起, 止],任一端可以为空(null,“10 万元以上”)。两端都填了、起比止大时自动对调,不用报错。\n * min、max、step、format、prefix、suffix、valueType 与外观(variant、size、radius)两端共用,同 NumberField。\n * 在 Field 里时:标签点了聚焦第一个框,组以标签为名称、关联说明与错误;两个框各自念“最小值”“最大值”。\n * 写了 name([起的名字, 止的名字])时两端各用一个隐藏的 input 随表单提交。\n *\n * 部件:Root(div)、Start、End(NumberField)、Separator(span)。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport type { VNodeChild } from 'vue';\nimport { useLang } from '../config/context';\nimport type { NumericFormat } from '../config/lang/format';\nimport { useFieldControl } from '../form/Field';\nimport { compareNumeric, type NumericKind, type NumericValue } from '../utils/data/numeric';\nimport { defaultLabelledBy, mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { fontSizeStyles } from '../utils/style/typography';\nimport type { ControlOptions } from './control';\nimport { NumberField, type NumberFieldProps } from './NumberField';\n\n/** 区间的值:[起, 止],空着的一端是 null */\nexport type NumberRange = readonly [NumericValue | null, NumericValue | null];\n\n/** 角色由组件管理;原生的 placeholder、prefix 属性让给两个框的占位与前缀 */\nexport type NumberRangeFieldManaged = 'role' | 'placeholder' | 'prefix';\n\n/** 未声明的属性落到外层 div 上 */\nexport interface NumberRangeFieldProps\n extends PartProps<'div', NumberRangeFieldManaged>, ControlOptions {\n /** [起, 止]:父级写 _value={range};任一端可以为 null */\n _value?: NumberRange;\n /** 两端共同的最小值 */\n min?: NumericValue;\n /** 两端共同的最大值 */\n max?: NumericValue;\n /** 步长,同 NumberField */\n step?: NumericValue;\n /** 显示格式,同 NumberField(scale 时按缩放后的单位输入) */\n format?: NumericFormat;\n /** 一开始为空时写回的类型,同 NumberField */\n valueType?: NumericKind;\n /** 框里文字前的内容 */\n prefix?: VNodeChild;\n /** 框里文字后的单位 */\n suffix?: VNodeChild;\n /** 两个框的占位文字,默认取语言包(最小值、最大值) */\n placeholder?: readonly [string, string];\n /** 两端中间的内容,默认“–” */\n separator?: VNodeChild;\n /** 表单提交时两端的 name */\n name?: readonly [string, string];\n /** 不可用 */\n disabled?: boolean;\n /** 只读 */\n readonly?: boolean;\n /** 必填 */\n required?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 起的那个框的部件属性 */\n propsStart?: Partial<NumberFieldProps>;\n /** 止的那个框的部件属性 */\n propsEnd?: Partial<NumberFieldProps>;\n /** 中间分隔的部件属性 */\n propsSeparator?: PartProps<'span', 'aria-hidden'>;\n}\n\n/** NumberRangeField 的样式:root、field、separator */\nexport const numberRangeFieldStyles = css.create({\n root: { display: 'flex', alignItems: 'center', gap: '_sm', minWidth: 0 },\n field: { flexGrow: 1, flexBasis: 0, minWidth: 0 },\n separator: { flexShrink: 0, color: '_textMuted' },\n});\n\n/** 数值区间:两个数字框,起比止大时自动对调 */\nexport const NumberRangeField = _component(\n ({\n _value: value = _bindable<NumberRange>([null, null]),\n min,\n max,\n step,\n format,\n valueType,\n prefix,\n suffix,\n placeholder,\n separator = '–',\n name,\n variant,\n size,\n radius,\n disabled,\n readonly,\n required,\n invalid,\n id,\n propsStart,\n propsEnd,\n propsSeparator,\n ...rest\n }: NumberRangeFieldProps) => {\n const lang = useLang();\n const control = useFieldControl(() => ({ id, invalid, required, disabled, readonly }));\n\n /** 改一端:两端都有值且起比止大时对调 */\n function set(index: 0 | 1, next: NumericValue | null) {\n const pair: [NumericValue | null, NumericValue | null] = [value[0], value[1]];\n pair[index] = next;\n const [start, end] = pair;\n value = start != null && end != null && compareNumeric(start, end) > 0 ? [end, start] : pair;\n }\n\n return () => {\n const state = control();\n const shared = {\n min,\n max,\n step,\n format,\n valueType,\n prefix,\n suffix,\n variant,\n size,\n radius,\n disabled: state.disabled,\n readonly: state.readonly,\n invalid: state.invalid,\n stepper: false,\n propsRoot: { sx: numberRangeFieldStyles.field },\n };\n return (\n <div\n {...mergeAttrs<'div'>(\n // 分隔符跟两个框一样大\n { sx: [numberRangeFieldStyles.root, size && fontSizeStyles[size]] },\n rest,\n {\n role: 'group',\n 'aria-labelledby': defaultLabelledBy(rest, state.labelledBy),\n 'aria-describedby': state.describedBy,\n },\n )}\n data-disabled={state.disabled || undefined}\n data-invalid={state.invalid || undefined}\n >\n <NumberField\n {...mergePartProps<NumberFieldProps>(\n { ...shared, placeholder: placeholder?.[0] ?? lang.sliderMin },\n propsStart,\n {\n // 标签的 for 指向第一个框:点标签聚焦“起”\n id: state.id,\n required: state.required,\n name: name?.[0],\n 'aria-label': lang.sliderMin,\n },\n )}\n _value={value[0]}\n onUpdate:_value={(next: NumericValue | null) => set(0, next)}\n />\n <span\n {...mergeAttrs<'span'>({ sx: numberRangeFieldStyles.separator }, propsSeparator, {\n 'aria-hidden': true,\n })}\n >\n {separator}\n </span>\n <NumberField\n {...mergePartProps<NumberFieldProps>(\n { ...shared, placeholder: placeholder?.[1] ?? lang.sliderMax },\n propsEnd,\n {\n id: state.id ? `${state.id}-end` : undefined,\n name: name?.[1],\n 'aria-label': lang.sliderMax,\n },\n )}\n _value={value[1]}\n onUpdate:_value={(next: NumericValue | null) => set(1, next)}\n />\n </div>\n );\n };\n },\n);\n"],"mappings":";;;;;;;;;;;;;;AA0EA,IAAa,yBAAyB,IAAI,OAAO;CAC/C,MAAM;EAAE,SAAS;EAAQ,YAAY;EAAU,KAAK;EAAO,UAAU;CAAE;CACvE,OAAO;EAAE,UAAU;EAAG,WAAW;EAAG,UAAU;CAAE;CAChD,WAAW;EAAE,YAAY;EAAG,OAAO;CAAa;AAClD,CAAC;;AAGD,IAAa,mBAAmB,0BAC9B,QAAA,SAwB6B;CAAA,MAAA,SAAA,CAAA;CAAA,MAAA,QAAAA,SAAA,QAAA,gBAvBY,CAAC,MAAM,IAAI,GAAC,KAAA,IAAA;CAAA,MAAA,MAAAC,aAAA,QAAA,KAAA;CAAA,MAAA,MAAAA,aAAA,QAAA,KAAA;CAAA,MAAA,OAAAA,aAAA,QAAA,MAAA;CAAA,MAAA,SAAAA,aAAA,QAAA,QAAA;CAAA,MAAA,YAAAA,aAAA,QAAA,WAAA;CAAA,MAAA,SAAAA,aAAA,QAAA,QAAA;CAAA,MAAA,SAAAA,aAAA,QAAA,QAAA;CAAA,MAAA,cAAAA,aAAA,QAAA,aAAA;CAAA,MAAA,YAAAA,aAAA,QAAA,aASvC,GAAG;CAAA,MAAA,OAAAA,aAAA,QAAA,MAAA;CAAA,MAAA,UAAAA,aAAA,QAAA,SAAA;CAAA,MAAA,OAAAA,aAAA,QAAA,MAAA;CAAA,MAAA,SAAAA,aAAA,QAAA,QAAA;CAAA,MAAA,WAAAA,aAAA,QAAA,UAAA;CAAA,MAAA,WAAAA,aAAA,QAAA,UAAA;CAAA,MAAA,WAAAA,aAAA,QAAA,UAAA;CAAA,MAAA,UAAAA,aAAA,QAAA,SAAA;CAAA,MAAA,KAAAA,aAAA,QAAA,IAAA;CAAA,MAAA,aAAAA,aAAA,QAAA,YAAA;CAAA,MAAA,WAAAA,aAAA,QAAA,UAAA;CAAA,MAAA,iBAAAA,aAAA,QAAA,gBAAA;CAAA,MAaZ,OAAIC,UAAA,QAAA,MAAA,IAAA;CAEP,MAAM,OAAO,QAAQ;CACrB,MAAM,UAAU,uBAAuB;EAAE,IAAE,GAAA;EAAE,SAAO,QAAA;EAAE,UAAQ,SAAA;EAAE,UAAQ,SAAA;EAAE,UAAQ,SAAA;CAAC,EAAE;;CAGrF,SAAS,IAAI,OAAc,MAA2B;EACpD,MAAM,OAAmD,CAAC,MAAA,MAAM,IAAI,MAAA,MAAM,EAAE;EAC5E,KAAK,SAAS;EACd,MAAM,CAAC,OAAO,OAAO;EACrB,MAAA,QAAQ,SAAS,QAAQ,OAAO,QAAQ,eAAe,OAAO,GAAG,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI;CAC1F;CAEA,aAAa;EACX,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS;GACb,KAAG,IAAA;GACH,KAAG,IAAA;GACH,MAAI,KAAA;GACJ,QAAM,OAAA;GACN,WAAS,UAAA;GACT,QAAM,OAAA;GACN,QAAM,OAAA;GACN,SAAO,QAAA;GACP,MAAI,KAAA;GACJ,QAAM,OAAA;GACN,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,SAAS,MAAM;GACf,SAAS;GACT,WAAW,EAAE,IAAI,uBAAuB,MAAM;EAChD;EACA,OAAAC,YAAA,OAAAC,
|
|
1
|
+
{"version":3,"file":"NumberRangeField.js","names":["_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_createVNode","_mergeProps"],"sources":["../../src/input/NumberRangeField.tsx"],"sourcesContent":["/**\n * 数值区间(div role=\"group\"):两个 NumberField,最小到最大。预期收益量级、金额范围、数量区间的录入与筛选。\n *\n * <Field label=\"预期收益量级\">\n * <NumberRangeField _value={range} format={{ scale: 10_000 }} suffix=\"万元\" min={0} />\n * </Field>\n *\n * value 是 [起, 止],任一端可以为空(null,“10 万元以上”)。两端都填了、起比止大时自动对调,不用报错。\n * min、max、step、format、prefix、suffix、valueType 与外观(variant、size、radius)两端共用,同 NumberField。\n * 在 Field 里时:标签点了聚焦第一个框,组以标签为名称、关联说明与错误;两个框各自念“最小值”“最大值”。\n * 写了 name([起的名字, 止的名字])时两端各用一个隐藏的 input 随表单提交。\n *\n * 部件:Root(div)、Start、End(NumberField)、Separator(span)。\n */\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport type { VNodeChild } from 'vue';\nimport { useLang } from '../config/context';\nimport type { NumericFormat } from '../config/lang/format';\nimport { useFieldControl } from '../form/Field';\nimport { compareNumeric, type NumericKind, type NumericValue } from '../utils/data/numeric';\nimport { defaultLabelledBy, mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { fontSizeStyles } from '../utils/style/typography';\nimport type { ControlOptions } from './control';\nimport { NumberField, type NumberFieldProps } from './NumberField';\n\n/** 区间的值:[起, 止],空着的一端是 null */\nexport type NumberRange = readonly [NumericValue | null, NumericValue | null];\n\n/** 角色由组件管理;原生的 placeholder、prefix 属性让给两个框的占位与前缀 */\nexport type NumberRangeFieldManaged = 'role' | 'placeholder' | 'prefix';\n\n/** 未声明的属性落到外层 div 上 */\nexport interface NumberRangeFieldProps\n extends PartProps<'div', NumberRangeFieldManaged>, ControlOptions {\n /** [起, 止]:父级写 _value={range};任一端可以为 null */\n _value?: NumberRange;\n /** 两端共同的最小值 */\n min?: NumericValue;\n /** 两端共同的最大值 */\n max?: NumericValue;\n /** 步长,同 NumberField */\n step?: NumericValue;\n /** 显示格式,同 NumberField(scale 时按缩放后的单位输入) */\n format?: NumericFormat;\n /** 一开始为空时写回的类型,同 NumberField */\n valueType?: NumericKind;\n /** 框里文字前的内容 */\n prefix?: VNodeChild;\n /** 框里文字后的单位 */\n suffix?: VNodeChild;\n /** 两个框的占位文字,默认取语言包(最小值、最大值) */\n placeholder?: readonly [string, string];\n /** 两端中间的内容,默认“–” */\n separator?: VNodeChild;\n /** 表单提交时两端的 name */\n name?: readonly [string, string];\n /** 不可用 */\n disabled?: boolean;\n /** 只读 */\n readonly?: boolean;\n /** 必填 */\n required?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 起的那个框的部件属性 */\n propsStart?: Partial<NumberFieldProps>;\n /** 止的那个框的部件属性 */\n propsEnd?: Partial<NumberFieldProps>;\n /** 中间分隔的部件属性 */\n propsSeparator?: PartProps<'span', 'aria-hidden'>;\n}\n\n/** NumberRangeField 的样式:root、field、separator */\nexport const numberRangeFieldStyles = css.create({\n root: { display: 'flex', alignItems: 'center', gap: '_sm', minWidth: 0 },\n field: { flexGrow: 1, flexBasis: 0, minWidth: 0 },\n separator: { flexShrink: 0, color: '_textMuted' },\n});\n\n/** 数值区间:两个数字框,起比止大时自动对调 */\nexport const NumberRangeField = _component(\n ({\n _value: value = _bindable<NumberRange>([null, null]),\n min,\n max,\n step,\n format,\n valueType,\n prefix,\n suffix,\n placeholder,\n separator = '–',\n name,\n variant,\n size,\n radius,\n disabled,\n readonly,\n required,\n invalid,\n id,\n propsStart,\n propsEnd,\n propsSeparator,\n ...rest\n }: NumberRangeFieldProps) => {\n const lang = useLang();\n const control = useFieldControl(() => ({ id, invalid, required, disabled, readonly }));\n\n /** 改一端:两端都有值且起比止大时对调 */\n function set(index: 0 | 1, next: NumericValue | null) {\n const pair: [NumericValue | null, NumericValue | null] = [value[0], value[1]];\n pair[index] = next;\n const [start, end] = pair;\n value = start != null && end != null && compareNumeric(start, end) > 0 ? [end, start] : pair;\n }\n\n return () => {\n const state = control();\n const shared = {\n min,\n max,\n step,\n format,\n valueType,\n prefix,\n suffix,\n variant,\n size,\n radius,\n disabled: state.disabled,\n readonly: state.readonly,\n invalid: state.invalid,\n stepper: false,\n propsRoot: { sx: numberRangeFieldStyles.field },\n };\n return (\n <div\n {...mergeAttrs<'div'>(\n // 分隔符跟两个框一样大\n { sx: [numberRangeFieldStyles.root, size && fontSizeStyles[size]] },\n rest,\n {\n role: 'group',\n 'aria-labelledby': defaultLabelledBy(rest, state.labelledBy),\n 'aria-describedby': state.describedBy,\n },\n )}\n data-disabled={state.disabled || undefined}\n data-invalid={state.invalid || undefined}\n >\n <NumberField\n {...mergePartProps<NumberFieldProps>(\n { ...shared, placeholder: placeholder?.[0] ?? lang.sliderMin },\n propsStart,\n {\n // 标签的 for 指向第一个框:点标签聚焦“起”\n id: state.id,\n required: state.required,\n name: name?.[0],\n 'aria-label': lang.sliderMin,\n },\n )}\n _value={value[0]}\n onUpdate:_value={(next: NumericValue | null) => set(0, next)}\n />\n <span\n {...mergeAttrs<'span'>({ sx: numberRangeFieldStyles.separator }, propsSeparator, {\n 'aria-hidden': true,\n })}\n >\n {separator}\n </span>\n <NumberField\n {...mergePartProps<NumberFieldProps>(\n { ...shared, placeholder: placeholder?.[1] ?? lang.sliderMax },\n propsEnd,\n {\n id: state.id ? `${state.id}-end` : undefined,\n name: name?.[1],\n 'aria-label': lang.sliderMax,\n },\n )}\n _value={value[1]}\n onUpdate:_value={(next: NumericValue | null) => set(1, next)}\n />\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
package/dist/input/OtpField.js
CHANGED
|
@@ -2,8 +2,8 @@ import { t } from "../config/theme/tokens.js";
|
|
|
2
2
|
import { mergeAttrs } from "../utils/props.js";
|
|
3
3
|
import { fontSizeStyles } from "../utils/style/typography.js";
|
|
4
4
|
import { useFieldControl } from "../form/Field.js";
|
|
5
|
-
import { createVNode,
|
|
6
|
-
import { _component, bindable, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
5
|
+
import { createVNode, ref } from "vue";
|
|
6
|
+
import { _component, bindable, mergeJsxProps, readonlyProp, restProps } from "@zerodep-dev/vue-tsx-pro-tsx";
|
|
7
7
|
import * as css from "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
8
8
|
//#region src/input/OtpField.tsx
|
|
9
9
|
/** 能输入的字符 */
|
|
@@ -134,13 +134,13 @@ var OtpField = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
134
134
|
const chars = value.value.split("");
|
|
135
135
|
const current = Math.min(chars.length, length.value - 1);
|
|
136
136
|
const ends = groupEnds();
|
|
137
|
-
return createVNode("div",
|
|
137
|
+
return createVNode("div", mergeJsxProps(mergeAttrs({ sx: [otpFieldStyles.root, size.value && fontSizeStyles[size.value]] }, propsRoot.value), {
|
|
138
138
|
"data-invalid": state.invalid || void 0,
|
|
139
139
|
"data-disabled": state.disabled || void 0
|
|
140
140
|
}), [Array.from({ length: length.value }, (_, index) => {
|
|
141
141
|
const char = chars[index];
|
|
142
142
|
const active = focused.value && !state.readonly && index === current;
|
|
143
|
-
return [createVNode("div",
|
|
143
|
+
return [createVNode("div", mergeJsxProps({
|
|
144
144
|
"key": `slot-${index}`,
|
|
145
145
|
"aria-hidden": "true"
|
|
146
146
|
}, mergeAttrs({ sx: [
|
|
@@ -148,7 +148,7 @@ var OtpField = /*#__PURE__*/ _component((_props, _ctx) => {
|
|
|
148
148
|
state.invalid && otpFieldStyles.invalid,
|
|
149
149
|
active && otpFieldStyles.active,
|
|
150
150
|
state.disabled && otpFieldStyles.disabled
|
|
151
|
-
] }, propsSlot.value), { "data-state": char ? "filled" : "empty" }), [char ? mask.value ? "•" : char : active && createVNode("span", css.attrs(otpFieldStyles.caret), null)]), ends.has(index) && createVNode("span",
|
|
151
|
+
] }, propsSlot.value), { "data-state": char ? "filled" : "empty" }), [char ? mask.value ? "•" : char : active && createVNode("span", css.attrs(otpFieldStyles.caret), null)]), ends.has(index) && createVNode("span", mergeJsxProps({
|
|
152
152
|
"key": `separator-${index}`,
|
|
153
153
|
"aria-hidden": "true"
|
|
154
154
|
}, mergeAttrs({ sx: otpFieldStyles.separator }, propsSeparator.value)), null)];
|