@chenzy-design/svelte 0.1.0 → 0.2.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/components.json +1 -1
- package/package.json +7 -8
- package/src/_floating/use-floating.ts +0 -208
- package/src/anchor/Anchor.a11y.test.ts +0 -49
- package/src/anchor/Anchor.kbd.test.ts +0 -70
- package/src/anchor/Anchor.svelte +0 -359
- package/src/anchor/AnchorKbdFixture.svelte +0 -24
- package/src/anchor/index.ts +0 -3
- package/src/anchor/meta.ts +0 -70
- package/src/anchor/types.ts +0 -8
- package/src/autocomplete/AutoComplete.a11y.test.ts +0 -49
- package/src/autocomplete/AutoComplete.kbd.test.ts +0 -69
- package/src/autocomplete/AutoComplete.svelte +0 -706
- package/src/autocomplete/AutoCompleteKbdFixture.svelte +0 -24
- package/src/autocomplete/index.ts +0 -2
- package/src/autocomplete/meta.ts +0 -41
- package/src/avatar/Avatar.a11y.test.ts +0 -37
- package/src/avatar/Avatar.svelte +0 -315
- package/src/avatar/AvatarGroup.svelte +0 -142
- package/src/avatar/context.ts +0 -21
- package/src/avatar/index.ts +0 -10
- package/src/avatar/meta.ts +0 -73
- package/src/back-top/BackTop.a11y.test.ts +0 -40
- package/src/back-top/BackTop.svelte +0 -314
- package/src/back-top/index.ts +0 -2
- package/src/back-top/meta.ts +0 -108
- package/src/badge/Badge.a11y.test.ts +0 -35
- package/src/badge/Badge.svelte +0 -328
- package/src/badge/BadgeA11yFixture.svelte +0 -18
- package/src/badge/index.ts +0 -2
- package/src/badge/meta.ts +0 -62
- package/src/banner/Banner.a11y.test.ts +0 -48
- package/src/banner/Banner.svelte +0 -368
- package/src/banner/index.ts +0 -3
- package/src/banner/meta.ts +0 -121
- package/src/breadcrumb/Breadcrumb.a11y.test.ts +0 -35
- package/src/breadcrumb/Breadcrumb.kbd.test.ts +0 -45
- package/src/breadcrumb/Breadcrumb.svelte +0 -414
- package/src/breadcrumb/BreadcrumbKbdFixture.svelte +0 -23
- package/src/breadcrumb/Item.svelte +0 -77
- package/src/breadcrumb/context.ts +0 -27
- package/src/breadcrumb/index.ts +0 -21
- package/src/breadcrumb/meta.ts +0 -128
- package/src/breadcrumb/types.ts +0 -4
- package/src/button/Button.a11y.test.ts +0 -54
- package/src/button/Button.svelte +0 -176
- package/src/button/index.ts +0 -2
- package/src/button/meta.ts +0 -28
- package/src/calendar/Calendar.a11y.test.ts +0 -51
- package/src/calendar/Calendar.kbd.test.ts +0 -59
- package/src/calendar/Calendar.svelte +0 -1344
- package/src/calendar/CalendarKbdFixture.svelte +0 -23
- package/src/calendar/index.ts +0 -3
- package/src/calendar/meta.ts +0 -117
- package/src/card/Card.a11y.test.ts +0 -38
- package/src/card/Card.svelte +0 -341
- package/src/card/CardMeta.svelte +0 -60
- package/src/card/index.ts +0 -8
- package/src/card/meta.ts +0 -83
- package/src/carousel/Carousel.a11y.test.ts +0 -53
- package/src/carousel/Carousel.kbd.test.ts +0 -67
- package/src/carousel/Carousel.svelte +0 -806
- package/src/carousel/CarouselA11yFixture.svelte +0 -25
- package/src/carousel/CarouselKbdFixture.svelte +0 -24
- package/src/carousel/index.ts +0 -2
- package/src/carousel/meta.ts +0 -74
- package/src/cascader/Cascader.a11y.test.ts +0 -59
- package/src/cascader/Cascader.kbd.test.ts +0 -56
- package/src/cascader/Cascader.svelte +0 -1570
- package/src/cascader/CascaderKbdFixture.svelte +0 -31
- package/src/cascader/index.ts +0 -3
- package/src/cascader/meta.ts +0 -57
- package/src/cascader/types.ts +0 -9
- package/src/checkbox/Checkbox.svelte +0 -281
- package/src/checkbox/CheckboxGroup.a11y.test.ts +0 -43
- package/src/checkbox/CheckboxGroup.kbd.test.ts +0 -40
- package/src/checkbox/CheckboxGroup.svelte +0 -122
- package/src/checkbox/CheckboxGroupKbdFixture.svelte +0 -20
- package/src/checkbox/context.ts +0 -31
- package/src/checkbox/index.ts +0 -4
- package/src/checkbox/meta.ts +0 -67
- package/src/collapse/Collapse.a11y.test.ts +0 -53
- package/src/collapse/Collapse.kbd.test.ts +0 -89
- package/src/collapse/Collapse.svelte +0 -406
- package/src/collapse/CollapseA11yFixture.svelte +0 -27
- package/src/collapse/CollapseKbdFixture.svelte +0 -25
- package/src/collapse/CollapsePanel.svelte +0 -102
- package/src/collapse/context.ts +0 -54
- package/src/collapse/index.ts +0 -17
- package/src/collapse/meta.ts +0 -79
- package/src/collapse/types.ts +0 -5
- package/src/color-picker/ColorPicker.a11y.test.ts +0 -60
- package/src/color-picker/ColorPicker.kbd.test.ts +0 -73
- package/src/color-picker/ColorPicker.svelte +0 -985
- package/src/color-picker/ColorPickerKbdFixture.svelte +0 -25
- package/src/color-picker/index.ts +0 -2
- package/src/color-picker/meta.ts +0 -47
- package/src/config-provider/ConfigProvider.svelte +0 -227
- package/src/config-provider/context.ts +0 -29
- package/src/config-provider/index.ts +0 -48
- package/src/config-provider/meta.ts +0 -128
- package/src/date-picker/DatePicker.a11y.test.ts +0 -45
- package/src/date-picker/DatePicker.kbd.test.ts +0 -71
- package/src/date-picker/DatePicker.svelte +0 -1798
- package/src/date-picker/DatePickerKbdFixture.svelte +0 -26
- package/src/date-picker/RangePicker.kbd.test.ts +0 -69
- package/src/date-picker/RangePicker.svelte +0 -775
- package/src/date-picker/RangePickerKbdFixture.svelte +0 -30
- package/src/date-picker/index.ts +0 -4
- package/src/date-picker/meta.ts +0 -67
- package/src/date-picker/range-meta.ts +0 -44
- package/src/descriptions/Descriptions.a11y.test.ts +0 -33
- package/src/descriptions/Descriptions.svelte +0 -287
- package/src/descriptions/DescriptionsItem.svelte +0 -43
- package/src/descriptions/context.ts +0 -21
- package/src/descriptions/index.ts +0 -16
- package/src/descriptions/meta.ts +0 -45
- package/src/descriptions/types.ts +0 -6
- package/src/divider/Divider.a11y.test.ts +0 -23
- package/src/divider/Divider.svelte +0 -154
- package/src/divider/index.ts +0 -2
- package/src/divider/meta.ts +0 -23
- package/src/drawer/Drawer.a11y.test.ts +0 -41
- package/src/drawer/Drawer.kbd.test.ts +0 -67
- package/src/drawer/Drawer.svelte +0 -554
- package/src/drawer/DrawerKbdFixture.svelte +0 -28
- package/src/drawer/index.ts +0 -2
- package/src/drawer/meta.ts +0 -127
- package/src/dropdown/Dropdown.a11y.test.ts +0 -46
- package/src/dropdown/Dropdown.kbd.test.ts +0 -56
- package/src/dropdown/Dropdown.svelte +0 -573
- package/src/dropdown/DropdownA11yFixture.svelte +0 -24
- package/src/dropdown/DropdownDivider.svelte +0 -17
- package/src/dropdown/DropdownItem.svelte +0 -127
- package/src/dropdown/DropdownItemNode.svelte +0 -355
- package/src/dropdown/DropdownKbdFixture.svelte +0 -26
- package/src/dropdown/DropdownMenu.svelte +0 -24
- package/src/dropdown/DropdownSubMenu.svelte +0 -212
- package/src/dropdown/DropdownTitle.svelte +0 -34
- package/src/dropdown/context.ts +0 -13
- package/src/dropdown/index.ts +0 -36
- package/src/dropdown/meta.ts +0 -97
- package/src/dropdown/types.test.ts +0 -50
- package/src/dropdown/types.ts +0 -67
- package/src/empty/Empty.a11y.test.ts +0 -24
- package/src/empty/Empty.svelte +0 -304
- package/src/empty/index.ts +0 -2
- package/src/empty/meta.ts +0 -41
- package/src/form/Field.svelte +0 -492
- package/src/form/FieldArray.svelte +0 -75
- package/src/form/Form.a11y.test.ts +0 -39
- package/src/form/Form.svelte +0 -243
- package/src/form/FormA11yFixture.svelte +0 -31
- package/src/form/FormCascader.svelte +0 -82
- package/src/form/FormCheckbox.svelte +0 -83
- package/src/form/FormDatePicker.svelte +0 -80
- package/src/form/FormErrorMessage.svelte +0 -44
- package/src/form/FormInput.svelte +0 -95
- package/src/form/FormLabel.svelte +0 -46
- package/src/form/FormRadio.svelte +0 -83
- package/src/form/FormRating.svelte +0 -85
- package/src/form/FormSection.svelte +0 -45
- package/src/form/FormSelect.svelte +0 -96
- package/src/form/FormSlider.svelte +0 -95
- package/src/form/FormSlot.svelte +0 -51
- package/src/form/FormSwitch.svelte +0 -82
- package/src/form/FormTagInput.svelte +0 -79
- package/src/form/FormTreeSelect.svelte +0 -85
- package/src/form/FormUpload.svelte +0 -92
- package/src/form/context.ts +0 -47
- package/src/form/index.ts +0 -22
- package/src/form/meta.ts +0 -85
- package/src/grid/Col.svelte +0 -142
- package/src/grid/Grid.a11y.test.ts +0 -14
- package/src/grid/GridA11yFixture.svelte +0 -12
- package/src/grid/Row.svelte +0 -116
- package/src/grid/index.ts +0 -3
- package/src/grid/meta.ts +0 -72
- package/src/grid/use-breakpoint.svelte.ts +0 -48
- package/src/highlight/Highlight.a11y.test.ts +0 -25
- package/src/highlight/Highlight.svelte +0 -99
- package/src/highlight/index.ts +0 -2
- package/src/highlight/meta.ts +0 -80
- package/src/icon/Icon.svelte +0 -156
- package/src/icon/index.ts +0 -2
- package/src/icon/meta.ts +0 -41
- package/src/image/Image.a11y.test.ts +0 -30
- package/src/image/Image.svelte +0 -387
- package/src/image/ImagePreview.kbd.test.ts +0 -52
- package/src/image/ImagePreview.svelte +0 -309
- package/src/image/ImagePreviewGroup.svelte +0 -93
- package/src/image/ImagePreviewKbdFixture.svelte +0 -32
- package/src/image/context.ts +0 -26
- package/src/image/index.ts +0 -15
- package/src/image/meta.ts +0 -131
- package/src/index.ts +0 -208
- package/src/input/Input.a11y.test.ts +0 -58
- package/src/input/Input.svelte +0 -505
- package/src/input/index.ts +0 -2
- package/src/input/meta.ts +0 -40
- package/src/input-number/InputNumber.a11y.test.ts +0 -43
- package/src/input-number/InputNumber.kbd.test.ts +0 -41
- package/src/input-number/InputNumber.svelte +0 -674
- package/src/input-number/InputNumberKbdFixture.svelte +0 -15
- package/src/input-number/index.ts +0 -2
- package/src/input-number/meta.ts +0 -65
- package/src/layout/Content.svelte +0 -37
- package/src/layout/Footer.svelte +0 -49
- package/src/layout/Header.svelte +0 -46
- package/src/layout/Layout.a11y.test.ts +0 -34
- package/src/layout/Layout.svelte +0 -60
- package/src/layout/LayoutA11yFixture.svelte +0 -21
- package/src/layout/Sider.svelte +0 -207
- package/src/layout/index.ts +0 -26
- package/src/layout/meta.ts +0 -116
- package/src/list/List.a11y.test.ts +0 -69
- package/src/list/List.bench.ts +0 -26
- package/src/list/List.kbd.test.ts +0 -71
- package/src/list/List.svelte +0 -920
- package/src/list/ListA11yFixture.svelte +0 -38
- package/src/list/ListBenchFixture.svelte +0 -22
- package/src/list/ListItem.svelte +0 -175
- package/src/list/ListKbdFixture.svelte +0 -31
- package/src/list/ListMeta.svelte +0 -73
- package/src/list/context.ts +0 -39
- package/src/list/index.ts +0 -21
- package/src/list/meta.ts +0 -99
- package/src/locale-provider/LocaleProvider.svelte +0 -110
- package/src/locale-provider/context.ts +0 -22
- package/src/locale-provider/index.ts +0 -21
- package/src/locale-provider/meta.ts +0 -93
- package/src/locale-provider/use-locale.ts +0 -25
- package/src/lottie-icon/LottieIcon.svelte +0 -330
- package/src/lottie-icon/index.ts +0 -9
- package/src/lottie-icon/meta.ts +0 -162
- package/src/menu/Menu.a11y.test.ts +0 -54
- package/src/menu/Menu.kbd.test.ts +0 -69
- package/src/menu/Menu.svelte +0 -762
- package/src/menu/MenuKbdFixture.svelte +0 -22
- package/src/menu/MenuPopupNode.svelte +0 -525
- package/src/menu/index.ts +0 -4
- package/src/menu/meta.ts +0 -137
- package/src/menu/types.ts +0 -120
- package/src/meta.test.ts +0 -173
- package/src/modal/ConfirmModal.svelte +0 -173
- package/src/modal/Modal.a11y.test.ts +0 -46
- package/src/modal/Modal.kbd.test.ts +0 -65
- package/src/modal/Modal.svelte +0 -511
- package/src/modal/ModalKbdFixture.svelte +0 -24
- package/src/modal/command.svelte.ts +0 -103
- package/src/modal/index.ts +0 -30
- package/src/modal/meta.ts +0 -165
- package/src/modal/z-stack.ts +0 -26
- package/src/notification/NotificationContainer.svelte +0 -192
- package/src/notification/NotificationItem.a11y.test.ts +0 -73
- package/src/notification/NotificationItem.svelte +0 -368
- package/src/notification/index.ts +0 -9
- package/src/notification/meta.ts +0 -120
- package/src/notification/store.ts +0 -191
- package/src/overflow-list/OverflowList.a11y.test.ts +0 -46
- package/src/overflow-list/OverflowList.kbd.test.ts +0 -34
- package/src/overflow-list/OverflowList.svelte +0 -539
- package/src/overflow-list/OverflowListA11yFixture.svelte +0 -22
- package/src/overflow-list/OverflowListKbdFixture.svelte +0 -21
- package/src/overflow-list/index.ts +0 -2
- package/src/overflow-list/meta.ts +0 -138
- package/src/pagination/Pagination.a11y.test.ts +0 -62
- package/src/pagination/Pagination.kbd.test.ts +0 -66
- package/src/pagination/Pagination.svelte +0 -428
- package/src/pagination/PaginationKbdFixture.svelte +0 -15
- package/src/pagination/index.ts +0 -2
- package/src/pagination/meta.ts +0 -73
- package/src/popconfirm/Popconfirm.a11y.test.ts +0 -46
- package/src/popconfirm/Popconfirm.kbd.test.ts +0 -42
- package/src/popconfirm/Popconfirm.svelte +0 -724
- package/src/popconfirm/PopconfirmA11yFixture.svelte +0 -34
- package/src/popconfirm/PopconfirmKbdFixture.svelte +0 -17
- package/src/popconfirm/index.ts +0 -2
- package/src/popconfirm/meta.ts +0 -200
- package/src/popover/Popover.a11y.test.ts +0 -58
- package/src/popover/Popover.kbd.test.ts +0 -42
- package/src/popover/Popover.svelte +0 -643
- package/src/popover/PopoverA11yFixture.svelte +0 -35
- package/src/popover/PopoverKbdFixture.svelte +0 -18
- package/src/popover/index.ts +0 -2
- package/src/popover/meta.ts +0 -103
- package/src/progress/Progress.a11y.test.ts +0 -37
- package/src/progress/Progress.svelte +0 -423
- package/src/progress/index.ts +0 -3
- package/src/progress/meta.ts +0 -132
- package/src/radio/Radio.svelte +0 -330
- package/src/radio/RadioGroup.a11y.test.ts +0 -43
- package/src/radio/RadioGroup.kbd.test.ts +0 -75
- package/src/radio/RadioGroup.svelte +0 -199
- package/src/radio/RadioGroupKbdFixture.svelte +0 -21
- package/src/radio/context.ts +0 -42
- package/src/radio/index.ts +0 -4
- package/src/radio/meta.ts +0 -68
- package/src/rating/Rating.a11y.test.ts +0 -41
- package/src/rating/Rating.kbd.test.ts +0 -48
- package/src/rating/Rating.svelte +0 -407
- package/src/rating/RatingKbdFixture.svelte +0 -15
- package/src/rating/index.ts +0 -2
- package/src/rating/meta.ts +0 -40
- package/src/resize-observer/ResizeObserver.svelte +0 -169
- package/src/resize-observer/action.ts +0 -80
- package/src/resize-observer/index.ts +0 -9
- package/src/resize-observer/meta.ts +0 -102
- package/src/scroll-list/ScrollColumn.svelte +0 -592
- package/src/scroll-list/ScrollList.svelte +0 -257
- package/src/scroll-list/index.ts +0 -4
- package/src/scroll-list/meta.ts +0 -93
- package/src/scroll-list/types.ts +0 -18
- package/src/select/Select.a11y.test.ts +0 -59
- package/src/select/Select.kbd.test.ts +0 -59
- package/src/select/Select.svelte +0 -1155
- package/src/select/SelectKbdFixture.svelte +0 -29
- package/src/select/index.ts +0 -3
- package/src/select/meta.ts +0 -78
- package/src/select/types.ts +0 -2
- package/src/side-sheet/SideSheet.a11y.test.ts +0 -53
- package/src/side-sheet/SideSheet.kbd.test.ts +0 -63
- package/src/side-sheet/SideSheet.svelte +0 -659
- package/src/side-sheet/SideSheetKbdFixture.svelte +0 -30
- package/src/side-sheet/index.ts +0 -2
- package/src/side-sheet/meta.ts +0 -116
- package/src/skeleton/Skeleton.a11y.test.ts +0 -35
- package/src/skeleton/Skeleton.svelte +0 -131
- package/src/skeleton/SkeletonAvatar.svelte +0 -82
- package/src/skeleton/SkeletonButton.svelte +0 -90
- package/src/skeleton/SkeletonImage.svelte +0 -100
- package/src/skeleton/SkeletonParagraph.svelte +0 -103
- package/src/skeleton/SkeletonTitle.svelte +0 -74
- package/src/skeleton/context.ts +0 -11
- package/src/skeleton/index.ts +0 -8
- package/src/skeleton/meta.ts +0 -65
- package/src/slider/Slider.a11y.test.ts +0 -44
- package/src/slider/Slider.kbd.test.ts +0 -74
- package/src/slider/Slider.svelte +0 -716
- package/src/slider/SliderKbdFixture.svelte +0 -21
- package/src/slider/index.ts +0 -2
- package/src/slider/meta.ts +0 -55
- package/src/space/Space.a11y.test.ts +0 -22
- package/src/space/Space.svelte +0 -109
- package/src/space/SpaceA11yFixture.svelte +0 -17
- package/src/space/index.ts +0 -2
- package/src/space/meta.ts +0 -27
- package/src/spin/Spin.a11y.test.ts +0 -29
- package/src/spin/Spin.svelte +0 -293
- package/src/spin/SpinA11yFixture.svelte +0 -18
- package/src/spin/index.ts +0 -2
- package/src/spin/meta.ts +0 -72
- package/src/steps/Steps.a11y.test.ts +0 -39
- package/src/steps/Steps.kbd.test.ts +0 -73
- package/src/steps/Steps.svelte +0 -392
- package/src/steps/StepsKbdFixture.svelte +0 -21
- package/src/steps/index.ts +0 -3
- package/src/steps/meta.ts +0 -59
- package/src/steps/types.ts +0 -8
- package/src/switch/Switch.a11y.test.ts +0 -46
- package/src/switch/Switch.kbd.test.ts +0 -40
- package/src/switch/Switch.svelte +0 -310
- package/src/switch/SwitchKbdFixture.svelte +0 -22
- package/src/switch/index.ts +0 -2
- package/src/switch/meta.ts +0 -44
- package/src/table/Table.a11y.test.ts +0 -70
- package/src/table/Table.bench.ts +0 -51
- package/src/table/Table.kbd.test.ts +0 -94
- package/src/table/Table.svelte +0 -2327
- package/src/table/TableKbdFixture.svelte +0 -35
- package/src/table/grid-nav.test.ts +0 -173
- package/src/table/grid-nav.ts +0 -146
- package/src/table/index.ts +0 -13
- package/src/table/meta.ts +0 -121
- package/src/table/types.ts +0 -133
- package/src/tabs/TabPane.svelte +0 -92
- package/src/tabs/Tabs.a11y.test.ts +0 -47
- package/src/tabs/Tabs.kbd.test.ts +0 -74
- package/src/tabs/Tabs.svelte +0 -1128
- package/src/tabs/TabsA11yFixture.svelte +0 -20
- package/src/tabs/TabsKbdFixture.svelte +0 -25
- package/src/tabs/context.ts +0 -43
- package/src/tabs/index.ts +0 -14
- package/src/tabs/meta.ts +0 -152
- package/src/tabs/types.ts +0 -6
- package/src/tag/Tag.a11y.test.ts +0 -33
- package/src/tag/Tag.svelte +0 -366
- package/src/tag/TagA11yFixture.svelte +0 -18
- package/src/tag/index.ts +0 -2
- package/src/tag/meta.ts +0 -92
- package/src/tag-input/TagInput.a11y.test.ts +0 -41
- package/src/tag-input/TagInput.svelte +0 -544
- package/src/tag-input/index.ts +0 -2
- package/src/tag-input/meta.ts +0 -43
- package/src/tag-input/reorder.test.ts +0 -59
- package/src/tag-input/reorder.ts +0 -41
- package/src/test-utils/LocaleHarness.svelte +0 -29
- package/src/test-utils/a11y.ts +0 -108
- package/src/test-utils/kbd.ts +0 -52
- package/src/textarea/TextArea.a11y.test.ts +0 -44
- package/src/textarea/TextArea.svelte +0 -420
- package/src/textarea/index.ts +0 -2
- package/src/textarea/meta.ts +0 -104
- package/src/time-picker/TimePicker.a11y.test.ts +0 -45
- package/src/time-picker/TimePicker.kbd.test.ts +0 -70
- package/src/time-picker/TimePicker.svelte +0 -1023
- package/src/time-picker/TimePickerKbdFixture.svelte +0 -23
- package/src/time-picker/index.ts +0 -2
- package/src/time-picker/meta.ts +0 -64
- package/src/timeline/Timeline.a11y.test.ts +0 -54
- package/src/timeline/Timeline.svelte +0 -599
- package/src/timeline/TimelineItem.svelte +0 -164
- package/src/timeline/context.ts +0 -28
- package/src/timeline/index.ts +0 -16
- package/src/timeline/meta.ts +0 -129
- package/src/timeline/types.ts +0 -9
- package/src/toast/Toast.a11y.test.ts +0 -67
- package/src/toast/ToastContainer.svelte +0 -184
- package/src/toast/ToastItem.svelte +0 -247
- package/src/toast/index.ts +0 -9
- package/src/toast/live-region.ts +0 -78
- package/src/toast/meta.ts +0 -95
- package/src/toast/store.ts +0 -187
- package/src/tooltip/Tooltip.a11y.test.ts +0 -41
- package/src/tooltip/Tooltip.kbd.test.ts +0 -37
- package/src/tooltip/Tooltip.svelte +0 -416
- package/src/tooltip/TooltipA11yFixture.svelte +0 -31
- package/src/tooltip/TooltipKbdFixture.svelte +0 -16
- package/src/tooltip/index.ts +0 -2
- package/src/tooltip/meta.ts +0 -59
- package/src/tooltip/placement.ts +0 -6
- package/src/transfer/Transfer.a11y.test.ts +0 -63
- package/src/transfer/Transfer.kbd.test.ts +0 -75
- package/src/transfer/Transfer.svelte +0 -1451
- package/src/transfer/TransferKbdFixture.svelte +0 -23
- package/src/transfer/group.test.ts +0 -55
- package/src/transfer/group.ts +0 -61
- package/src/transfer/index.ts +0 -3
- package/src/transfer/meta.ts +0 -55
- package/src/transfer/reorder.test.ts +0 -52
- package/src/transfer/reorder.ts +0 -42
- package/src/transfer/tree.test.ts +0 -44
- package/src/transfer/tree.ts +0 -52
- package/src/transfer/types.ts +0 -32
- package/src/tree/Tree.a11y.test.ts +0 -63
- package/src/tree/Tree.kbd.test.ts +0 -91
- package/src/tree/Tree.svelte +0 -1500
- package/src/tree/TreeKbdFixture.svelte +0 -30
- package/src/tree/index.ts +0 -3
- package/src/tree/meta.ts +0 -152
- package/src/tree-select/TreeSelect.a11y.test.ts +0 -52
- package/src/tree-select/TreeSelect.kbd.test.ts +0 -53
- package/src/tree-select/TreeSelect.svelte +0 -1611
- package/src/tree-select/TreeSelectKbdFixture.svelte +0 -20
- package/src/tree-select/index.ts +0 -3
- package/src/tree-select/meta.ts +0 -73
- package/src/tree-select/types.ts +0 -11
- package/src/tree-select/virtual.test.ts +0 -104
- package/src/typography/Link.svelte +0 -120
- package/src/typography/Paragraph.svelte +0 -92
- package/src/typography/Text.svelte +0 -93
- package/src/typography/Title.svelte +0 -91
- package/src/typography/Typography.a11y.test.ts +0 -34
- package/src/typography/TypographyA11yFixture.svelte +0 -27
- package/src/typography/TypographyBase.svelte +0 -807
- package/src/typography/index.ts +0 -16
- package/src/typography/meta.ts +0 -158
- package/src/upload/Upload.a11y.test.ts +0 -58
- package/src/upload/Upload.svelte +0 -1094
- package/src/upload/UploadA11yFixture.svelte +0 -23
- package/src/upload/index.ts +0 -3
- package/src/upload/meta.ts +0 -64
- package/src/upload/types.ts +0 -16
- package/src/virtual-list/VirtualList.svelte +0 -401
- package/src/virtual-list/index.ts +0 -2
- package/src/virtual-list/meta.ts +0 -106
|
@@ -1,1451 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Transfer — see specs/components/input/Transfer.spec.md
|
|
3
|
-
Basic subset: type='list' two-column transfer, checkbox multi-select,
|
|
4
|
-
move buttons, local search filter. Controlled / uncontrolled `value`.
|
|
5
|
-
groupList: `dataSource` accepts grouped `{ title, items }[]` (or flat items
|
|
6
|
-
with an optional `group` field) — each side renders group headers + items.
|
|
7
|
-
treeList: `dataSource` accepts `TransferTreeNode[]` (items with `children`).
|
|
8
|
-
The source panel renders a tree (indent + expand/collapse); checking a parent
|
|
9
|
-
conducts to all enabled leaf descendants (core `conduct`/`toggleCheck`), with
|
|
10
|
-
tri-state half-check. Only leaves migrate to the (flat) target panel; migrated
|
|
11
|
-
leaves are greyed/disabled in the source tree.
|
|
12
|
-
oneWay: left→right only — each target row has its own remove button; no
|
|
13
|
-
right→left batch button.
|
|
14
|
-
virtualize: `virtualize` (+`itemHeight`) renders only the in-viewport slice of
|
|
15
|
-
*both* flat panels (reuses core `fixedRange`); scrollTop is written by an
|
|
16
|
-
imperative rAF-throttled scroll listener into local $state, the visible range
|
|
17
|
-
is a pure $derived (红线 #2/#3). Falls back to full render for tree/group panels.
|
|
18
|
-
draggable: `draggable` lets target (selected) rows be reordered by mouse via
|
|
19
|
-
HTML5 DnD; the new order is computed by the `reorder` pure fn (红线 #2) and only
|
|
20
|
-
propagated via `onChange` — controlled `value` is never written (红线 #1).
|
|
21
|
-
remote onSearch: providing `onSearch(query, direction)` switches to remote mode
|
|
22
|
-
(debounced via `searchDebounce`, imperative timer + cleanup, 红线 #3); the parent
|
|
23
|
-
updates `dataSource`, local filtering is skipped, `loading` shows a spinner.
|
|
24
|
-
-->
|
|
25
|
-
<script lang="ts">
|
|
26
|
-
import { tick } from 'svelte';
|
|
27
|
-
import Checkbox from '../checkbox/Checkbox.svelte';
|
|
28
|
-
import Input from '../input/Input.svelte';
|
|
29
|
-
import Button from '../button/Button.svelte';
|
|
30
|
-
import { useLocale } from '../locale-provider/index.js';
|
|
31
|
-
import {
|
|
32
|
-
conduct,
|
|
33
|
-
toggleCheck,
|
|
34
|
-
flattenVisible,
|
|
35
|
-
collectExpandable,
|
|
36
|
-
computeFilteredKeys,
|
|
37
|
-
useId,
|
|
38
|
-
useLiveAnnouncer,
|
|
39
|
-
rovingKeyFromEvent,
|
|
40
|
-
nextRovingIndex,
|
|
41
|
-
type TreeNodeData,
|
|
42
|
-
type FlatNode,
|
|
43
|
-
} from '@chenzy-design/core';
|
|
44
|
-
import type { Snippet } from 'svelte';
|
|
45
|
-
import Pagination from '../pagination/Pagination.svelte';
|
|
46
|
-
import type {
|
|
47
|
-
TransferGroup,
|
|
48
|
-
TransferItem,
|
|
49
|
-
TransferRenderGroup,
|
|
50
|
-
TransferTreeNode,
|
|
51
|
-
} from './types.js';
|
|
52
|
-
import { fixedRange } from '@chenzy-design/core';
|
|
53
|
-
import { buildGroups, hasGroups, normalizeData } from './group.js';
|
|
54
|
-
import { isTreeData, flattenLeaves } from './tree.js';
|
|
55
|
-
import { computeInsertSide, reorder, type InsertSide } from './reorder.js';
|
|
56
|
-
import type { Attachment } from 'svelte/attachments';
|
|
57
|
-
|
|
58
|
-
type TransferKey = string | number;
|
|
59
|
-
type Size = 'small' | 'default' | 'large';
|
|
60
|
-
type Status = 'default' | 'warning' | 'error';
|
|
61
|
-
type TransferType = 'list' | 'groupList' | 'treeList';
|
|
62
|
-
|
|
63
|
-
/** emptyContent: custom empty state renderers */
|
|
64
|
-
interface EmptyContent {
|
|
65
|
-
left?: Snippet | string;
|
|
66
|
-
right?: Snippet | string;
|
|
67
|
-
search?: Snippet | string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** pagination config for left panel */
|
|
71
|
-
interface PaginationConfig {
|
|
72
|
-
pageSize?: number;
|
|
73
|
-
currentPage?: number;
|
|
74
|
-
defaultCurrentPage?: number;
|
|
75
|
-
onPageChange?: (page: number) => void;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Source panel header render args */
|
|
79
|
-
interface SourceHeaderProps {
|
|
80
|
-
totalContent: string;
|
|
81
|
-
allContent: string;
|
|
82
|
-
onAllClick: () => void;
|
|
83
|
-
title: string;
|
|
84
|
-
allChecked: boolean;
|
|
85
|
-
showButton: boolean;
|
|
86
|
-
leafOnlyNum?: number | undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Selected panel header render args */
|
|
90
|
-
interface SelectedHeaderProps {
|
|
91
|
-
totalContent: string;
|
|
92
|
-
allContent: string;
|
|
93
|
-
onAllClick: () => void;
|
|
94
|
-
title: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** renderSourcePanel args */
|
|
98
|
-
interface SourcePanelProps {
|
|
99
|
-
dataSource: TransferItem[];
|
|
100
|
-
selectedItems: TransferItem[];
|
|
101
|
-
onChange: (keys: TransferKey[]) => void;
|
|
102
|
-
onSearch: (v: string) => void;
|
|
103
|
-
inputValue: string;
|
|
104
|
-
searchResult: TransferItem[];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** renderSelectedPanel args */
|
|
108
|
-
interface SelectedPanelProps {
|
|
109
|
-
selectedItems: TransferItem[];
|
|
110
|
-
onRemove: (key: TransferKey) => void;
|
|
111
|
-
onSortEnd: (keys: TransferKey[]) => void;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface Props {
|
|
115
|
-
value?: TransferKey[];
|
|
116
|
-
defaultValue?: TransferKey[];
|
|
117
|
-
dataSource?: TransferItem[] | TransferGroup[] | TransferTreeNode[];
|
|
118
|
-
/** 'list' | 'groupList' | 'treeList' — controls source panel render mode. */
|
|
119
|
-
type?: TransferType;
|
|
120
|
-
filter?: boolean;
|
|
121
|
-
searchPlaceholder?: string;
|
|
122
|
-
size?: Size;
|
|
123
|
-
status?: Status;
|
|
124
|
-
disabled?: boolean;
|
|
125
|
-
showPanelTitle?: boolean;
|
|
126
|
-
titles?: [string, string];
|
|
127
|
-
/** Left→right only: target rows get a remove button, no right→left batch. */
|
|
128
|
-
oneWay?: boolean;
|
|
129
|
-
/**
|
|
130
|
-
* Reorder target (selected) rows by mouse drag (HTML5 DnD). New order is
|
|
131
|
-
* propagated only via `onChange` (controlled `value` is never written).
|
|
132
|
-
* Ignored in tree mode (target panel stays flat but DnD reorder applies).
|
|
133
|
-
*/
|
|
134
|
-
draggable?: boolean;
|
|
135
|
-
/**
|
|
136
|
-
* Virtualize both flat panels: render only the in-viewport slice for large
|
|
137
|
-
* lists. Reuses core `fixedRange`. Ignored for grouped/tree source panels.
|
|
138
|
-
*/
|
|
139
|
-
virtualize?: boolean;
|
|
140
|
-
/** Virtualized row height (px, default 32); must match the real row height. */
|
|
141
|
-
itemHeight?: number;
|
|
142
|
-
/**
|
|
143
|
-
* Remote search: providing this switches to remote mode — input is debounced
|
|
144
|
-
* (`searchDebounce` ms) then this is called with the query and which panel
|
|
145
|
-
* fired it; the parent updates `dataSource`, local filtering is skipped.
|
|
146
|
-
*/
|
|
147
|
-
onSearch?: (query: string, direction: 'left' | 'right') => void;
|
|
148
|
-
/** Remote loading: shows a spinner row in each panel list. */
|
|
149
|
-
loading?: boolean;
|
|
150
|
-
/** onSearch debounce in ms (default 300). */
|
|
151
|
-
searchDebounce?: number;
|
|
152
|
-
onChange?: (targetKeys: TransferKey[]) => void;
|
|
153
|
-
/** Custom empty state renderers for left/right/search empty states. */
|
|
154
|
-
emptyContent?: EmptyContent;
|
|
155
|
-
/** Extra props passed to the search Input component. */
|
|
156
|
-
inputProps?: Record<string, unknown>;
|
|
157
|
-
/** Props passed to the Tree component when type='treeList'. */
|
|
158
|
-
treeProps?: Record<string, unknown>;
|
|
159
|
-
/** When type='treeList', show full path in right panel selected items. */
|
|
160
|
-
showPath?: boolean;
|
|
161
|
-
/** Left panel pagination config (only for list/groupList). */
|
|
162
|
-
pagination?: PaginationConfig;
|
|
163
|
-
/** Called when a single item is checked (not bulk select). */
|
|
164
|
-
onSelect?: (item: TransferItem) => void;
|
|
165
|
-
/** Called when a single item is unchecked (not bulk select). */
|
|
166
|
-
onDeselect?: (item: TransferItem) => void;
|
|
167
|
-
/** Custom render for left panel source items. */
|
|
168
|
-
renderSourceItem?: Snippet<[{ item: TransferItem; onChange: () => void; checked: boolean }]>;
|
|
169
|
-
/** Custom render for right panel selected items. */
|
|
170
|
-
renderSelectedItem?: Snippet<[{ item: TransferItem; onRemove: () => void; sortableHandle?: unknown }]>;
|
|
171
|
-
/** Custom render for left panel header. */
|
|
172
|
-
renderSourceHeader?: Snippet<[SourceHeaderProps]>;
|
|
173
|
-
/** Custom render for right panel header. */
|
|
174
|
-
renderSelectedHeader?: Snippet<[SelectedHeaderProps]>;
|
|
175
|
-
/** Fully custom left panel renderer. */
|
|
176
|
-
renderSourcePanel?: Snippet<[SourcePanelProps]>;
|
|
177
|
-
/** Fully custom right panel renderer. */
|
|
178
|
-
renderSelectedPanel?: Snippet<[SelectedPanelProps]>;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let {
|
|
182
|
-
value,
|
|
183
|
-
defaultValue = [],
|
|
184
|
-
dataSource = [],
|
|
185
|
-
type = 'list',
|
|
186
|
-
filter = true,
|
|
187
|
-
searchPlaceholder,
|
|
188
|
-
size = 'default',
|
|
189
|
-
status = 'default',
|
|
190
|
-
disabled = false,
|
|
191
|
-
showPanelTitle = true,
|
|
192
|
-
titles,
|
|
193
|
-
oneWay = false,
|
|
194
|
-
draggable = false,
|
|
195
|
-
virtualize = false,
|
|
196
|
-
itemHeight = 32,
|
|
197
|
-
onSearch,
|
|
198
|
-
loading = false,
|
|
199
|
-
searchDebounce = 300,
|
|
200
|
-
onChange,
|
|
201
|
-
emptyContent,
|
|
202
|
-
inputProps,
|
|
203
|
-
treeProps: _treeProps,
|
|
204
|
-
showPath = false,
|
|
205
|
-
pagination,
|
|
206
|
-
onSelect,
|
|
207
|
-
onDeselect,
|
|
208
|
-
renderSourceItem,
|
|
209
|
-
renderSelectedItem,
|
|
210
|
-
renderSourceHeader,
|
|
211
|
-
renderSelectedHeader,
|
|
212
|
-
renderSourcePanel,
|
|
213
|
-
renderSelectedPanel,
|
|
214
|
-
}: Props = $props();
|
|
215
|
-
|
|
216
|
-
const loc = useLocale();
|
|
217
|
-
|
|
218
|
-
const announcer = useLiveAnnouncer();
|
|
219
|
-
const baseId = useId('cd-transfer');
|
|
220
|
-
const leftTitleId = `${baseId}-left-title`;
|
|
221
|
-
const rightTitleId = `${baseId}-right-title`;
|
|
222
|
-
|
|
223
|
-
const searchPlaceholderText = $derived(
|
|
224
|
-
searchPlaceholder ?? loc().t('Transfer.searchPlaceholder'),
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
const isControlled = $derived(value !== undefined);
|
|
228
|
-
let inner = $state<TransferKey[]>(getInitialValue());
|
|
229
|
-
// Controlled (`value=`): parent owns the value; we never write the prop, only
|
|
230
|
-
// propagate via `onChange`. Uncontrolled: keep our own state in sync.
|
|
231
|
-
const current = $derived(isControlled ? (value ?? []) : inner);
|
|
232
|
-
|
|
233
|
-
function getInitialValue(): TransferKey[] {
|
|
234
|
-
return [...defaultValue];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Local checked sets — purely local UI state, independent of `value`.
|
|
238
|
-
// In tree mode `leftChecked` is the *base* (leaf-level) checked set fed to
|
|
239
|
-
// core `conduct`/`toggleCheck`.
|
|
240
|
-
let leftChecked = $state<TransferKey[]>([]);
|
|
241
|
-
let rightChecked = $state<TransferKey[]>([]);
|
|
242
|
-
|
|
243
|
-
// --- Listbox roving focus + range-select anchors (a11y §6 / Listbox APG) ---
|
|
244
|
-
// Roving focus key per panel (single Tab stop each). null => first visible row
|
|
245
|
-
// is the Tab stop. Owned as $state here; DOM focus() happens in event handlers.
|
|
246
|
-
let leftFocusKey = $state<TransferKey | null>(null);
|
|
247
|
-
let rightFocusKey = $state<TransferKey | null>(null);
|
|
248
|
-
// Shift range-select anchors (plain bookkeeping, not reactive — 红线 #2).
|
|
249
|
-
let leftAnchor = -1;
|
|
250
|
-
let rightAnchor = -1;
|
|
251
|
-
// List roots captured via {@attach} for command focus() (非 render 期读 DOM).
|
|
252
|
-
let leftListEl = $state<HTMLUListElement | null>(null);
|
|
253
|
-
let rightListEl = $state<HTMLUListElement | null>(null);
|
|
254
|
-
|
|
255
|
-
// Local search queries — local UI state.
|
|
256
|
-
let leftQuery = $state('');
|
|
257
|
-
let rightQuery = $state('');
|
|
258
|
-
|
|
259
|
-
// --- Tree mode detection (treeList) -------------------------------------
|
|
260
|
-
// type='treeList' OR auto-detect via data shape (backward compat).
|
|
261
|
-
const isTree = $derived(type === 'treeList' || isTreeData(dataSource as readonly unknown[]));
|
|
262
|
-
const treeData = $derived(isTree ? (dataSource as TransferTreeNode[]) : []);
|
|
263
|
-
|
|
264
|
-
// Tree leaves flattened to flat items (the migratable units) for the target
|
|
265
|
-
// panel + label lookups. In tree mode this replaces the normalized list.
|
|
266
|
-
const treeLeaves = $derived(flattenLeaves(treeData));
|
|
267
|
-
|
|
268
|
-
// Normalize grouped `{ title, items }[]` or flat items into one flat list,
|
|
269
|
-
// tagging each item with `group`. Backward compatible: flat data is untouched.
|
|
270
|
-
const items = $derived(
|
|
271
|
-
isTree ? treeLeaves : normalizeData(dataSource as TransferItem[] | TransferGroup[]),
|
|
272
|
-
);
|
|
273
|
-
// type='groupList' forces grouped render; also auto-detect via data shape.
|
|
274
|
-
const grouped = $derived(!isTree && (type === 'groupList' || hasGroups(items)));
|
|
275
|
-
|
|
276
|
-
const leftItems = $derived(items.filter((item) => !current.includes(item.key)));
|
|
277
|
-
const rightItems = $derived(items.filter((item) => current.includes(item.key)));
|
|
278
|
-
|
|
279
|
-
// remote 模式:外部已按 query 更新 dataSource,本地不再过滤(对齐 Select)。
|
|
280
|
-
const isRemote = $derived(onSearch !== undefined);
|
|
281
|
-
|
|
282
|
-
function matches(label: string, query: string): boolean {
|
|
283
|
-
return label.toLowerCase().includes(query.trim().toLowerCase());
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const leftVisible = $derived(
|
|
287
|
-
filter && !isRemote && leftQuery
|
|
288
|
-
? leftItems.filter((i) => matches(i.label, leftQuery))
|
|
289
|
-
: leftItems,
|
|
290
|
-
);
|
|
291
|
-
const rightVisible = $derived(
|
|
292
|
-
filter && !isRemote && rightQuery
|
|
293
|
-
? rightItems.filter((i) => matches(i.label, rightQuery))
|
|
294
|
-
: rightItems,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
// --- Listbox roving math (flat panels only; tree/group panels stay non-listbox) ---
|
|
298
|
-
// Ordered visible row keys per panel — pure derivations (红线 #2: render 期只读).
|
|
299
|
-
const leftRowKeys = $derived(leftVisible.map((i) => i.key));
|
|
300
|
-
const rightRowKeys = $derived(rightVisible.map((i) => i.key));
|
|
301
|
-
|
|
302
|
-
function panelItems(side: 'left' | 'right'): TransferItem[] {
|
|
303
|
-
return side === 'left' ? leftVisible : rightVisible;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// 纯派生 tabindex:焦点行(或无焦点/失效时首行)为 0,其余 -1。失效焦点回退首行。
|
|
307
|
-
function rowTabindex(side: 'left' | 'right', key: TransferKey): 0 | -1 {
|
|
308
|
-
const focus = side === 'left' ? leftFocusKey : rightFocusKey;
|
|
309
|
-
const keys = side === 'left' ? leftRowKeys : rightRowKeys;
|
|
310
|
-
const active = focus != null && keys.includes(focus) ? focus : keys[0];
|
|
311
|
-
return active === key ? 0 : -1;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 命令式 focus()(事件回调内,非 render 期读 DOM)。
|
|
315
|
-
function focusRow(side: 'left' | 'right', key: TransferKey): void {
|
|
316
|
-
const root = side === 'left' ? leftListEl : rightListEl;
|
|
317
|
-
root
|
|
318
|
-
?.querySelector<HTMLElement>(`[data-transfer-key="${CSS.escape(String(key))}"]`)
|
|
319
|
-
?.focus();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Shift 连选:把 [from, to] 闭区间内非禁用项 toggle-on 进 checked 集合。
|
|
323
|
-
function setRangeChecked(side: 'left' | 'right', from: number, to: number): void {
|
|
324
|
-
const list = panelItems(side);
|
|
325
|
-
const lo = Math.min(from, to);
|
|
326
|
-
const hi = Math.max(from, to);
|
|
327
|
-
const cur = side === 'left' ? leftChecked : rightChecked;
|
|
328
|
-
const set = new Set(cur);
|
|
329
|
-
for (let i = lo; i <= hi; i += 1) {
|
|
330
|
-
const it = list[i];
|
|
331
|
-
if (it && !it.disabled && !disabled) set.add(it.key);
|
|
332
|
-
}
|
|
333
|
-
if (side === 'left') leftChecked = [...set];
|
|
334
|
-
else rightChecked = [...set];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// 行 keydown:↑↓/Home/End roving、Space 勾选、Enter 移动本面板已勾选、Shift+↑↓ 连选。
|
|
338
|
-
// 焦点 $state 在此写、命令式 focus() 也在此(非 render 期)。
|
|
339
|
-
function onRowKeydown(e: KeyboardEvent, side: 'left' | 'right', key: TransferKey): void {
|
|
340
|
-
const keys = side === 'left' ? leftRowKeys : rightRowKeys;
|
|
341
|
-
const list = panelItems(side);
|
|
342
|
-
const cur = keys.indexOf(key);
|
|
343
|
-
if (cur < 0) return;
|
|
344
|
-
const item = list[cur];
|
|
345
|
-
// Space:切换当前行勾选(禁用项为 no-op)。
|
|
346
|
-
if (e.key === ' ' || e.key === 'Spacebar') {
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
if (item && !item.disabled && !disabled) {
|
|
349
|
-
toggleChecked(side, key);
|
|
350
|
-
if (side === 'left') leftAnchor = cur;
|
|
351
|
-
else rightAnchor = cur;
|
|
352
|
-
}
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
// Enter:把本面板已勾选项移到对侧。
|
|
356
|
-
if (e.key === 'Enter') {
|
|
357
|
-
e.preventDefault();
|
|
358
|
-
if (side === 'left') moveToRight();
|
|
359
|
-
else moveToLeft();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
const intent = rovingKeyFromEvent(e.key);
|
|
363
|
-
if (!intent) return;
|
|
364
|
-
e.preventDefault();
|
|
365
|
-
const next = nextRovingIndex(cur, keys.length, intent);
|
|
366
|
-
const nextKey = keys[next];
|
|
367
|
-
if (nextKey == null) return;
|
|
368
|
-
if (side === 'left') leftFocusKey = nextKey;
|
|
369
|
-
else rightFocusKey = nextKey;
|
|
370
|
-
// Shift+方向键(仅 prev/next,不含 Home/End):以锚点为基准范围连选。
|
|
371
|
-
if (e.shiftKey && (intent === 'prev' || intent === 'next')) {
|
|
372
|
-
const anchor = side === 'left' ? leftAnchor : rightAnchor;
|
|
373
|
-
const base = anchor >= 0 ? anchor : cur;
|
|
374
|
-
if (side === 'left') leftAnchor = base;
|
|
375
|
-
else rightAnchor = base;
|
|
376
|
-
setRangeChecked(side, base, next);
|
|
377
|
-
} else if (!e.shiftKey) {
|
|
378
|
-
if (side === 'left') leftAnchor = next;
|
|
379
|
-
else rightAnchor = next;
|
|
380
|
-
}
|
|
381
|
-
focusRow(side, nextKey);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function onRowFocus(side: 'left' | 'right', key: TransferKey): void {
|
|
385
|
-
if (side === 'left') leftFocusKey = key;
|
|
386
|
-
else rightFocusKey = key;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 选项行点击:单击 toggle,Shift+click 以锚点为基准范围连选(与键盘一致)。
|
|
390
|
-
function onRowOptionClick(e: MouseEvent, side: 'left' | 'right', item: TransferItem): void {
|
|
391
|
-
if (disabled || item.disabled) return;
|
|
392
|
-
const keys = side === 'left' ? leftRowKeys : rightRowKeys;
|
|
393
|
-
const idx = keys.indexOf(item.key);
|
|
394
|
-
if (e.shiftKey) {
|
|
395
|
-
const anchor = side === 'left' ? leftAnchor : rightAnchor;
|
|
396
|
-
if (anchor >= 0) {
|
|
397
|
-
setRangeChecked(side, anchor, idx);
|
|
398
|
-
} else {
|
|
399
|
-
toggleChecked(side, item.key);
|
|
400
|
-
if (side === 'left') leftAnchor = idx;
|
|
401
|
-
else rightAnchor = idx;
|
|
402
|
-
}
|
|
403
|
-
} else {
|
|
404
|
-
toggleChecked(side, item.key);
|
|
405
|
-
if (side === 'left') leftAnchor = idx;
|
|
406
|
-
else rightAnchor = idx;
|
|
407
|
-
}
|
|
408
|
-
if (side === 'left') leftFocusKey = item.key;
|
|
409
|
-
else rightFocusKey = item.key;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// 选项行 root 捕获(多个 {@attach} 合法;避免 bind:this 三元)。
|
|
413
|
-
function captureList(side: 'left' | 'right'): Attachment<HTMLUListElement> {
|
|
414
|
-
return (el) => {
|
|
415
|
-
if (side === 'left') leftListEl = el;
|
|
416
|
-
else rightListEl = el;
|
|
417
|
-
return () => {
|
|
418
|
-
if (side === 'left') leftListEl = null;
|
|
419
|
-
else rightListEl = null;
|
|
420
|
-
};
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Render groups — pure derivations. Empty groups vanish because the input is
|
|
425
|
-
// the post-filter visible list. Fallback bucket reuses the panel title.
|
|
426
|
-
const sourceFallback = $derived(titles?.[0] ?? loc().t('Transfer.titleSource'));
|
|
427
|
-
const targetFallback = $derived(titles?.[1] ?? loc().t('Transfer.titleTarget'));
|
|
428
|
-
const leftGroups = $derived(buildGroups(leftVisible, sourceFallback));
|
|
429
|
-
const rightGroups = $derived(buildGroups(rightVisible, targetFallback));
|
|
430
|
-
|
|
431
|
-
// --- Tree-mode derivations (pure; render never writes state) -------------
|
|
432
|
-
const movedSet = $derived(new Set(current));
|
|
433
|
-
|
|
434
|
-
// Tree for the source panel: already-migrated leaves are disabled (greyed) so
|
|
435
|
-
// they cannot be re-checked (core `conduct` excludes disabled from auto-check).
|
|
436
|
-
function maskTree(nodes: TransferTreeNode[]): TreeNodeData[] {
|
|
437
|
-
return nodes.map((n) => {
|
|
438
|
-
if (n.children && n.children.length > 0) {
|
|
439
|
-
return { ...n, children: maskTree(n.children) } as TreeNodeData;
|
|
440
|
-
}
|
|
441
|
-
return { ...n, disabled: !!n.disabled || movedSet.has(n.key) } as TreeNodeData;
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
const sourceTree = $derived(maskTree(treeData));
|
|
445
|
-
|
|
446
|
-
// Expansion is local UI state. Default: expand all parents on first paint.
|
|
447
|
-
let expandedTouched = $state(false);
|
|
448
|
-
let innerExpanded = $state<Set<TransferKey>>(new Set());
|
|
449
|
-
const defaultExpanded = $derived(new Set(collectExpandable(sourceTree)));
|
|
450
|
-
const baseExpanded = $derived(expandedTouched ? innerExpanded : defaultExpanded);
|
|
451
|
-
|
|
452
|
-
// Search expands ancestors of matches without writing the base set (红线 #1/#2).
|
|
453
|
-
const treeSearch = $derived(filter && !isRemote ? leftQuery.trim().toLowerCase() : '');
|
|
454
|
-
const treeFiltered = $derived(
|
|
455
|
-
treeSearch
|
|
456
|
-
? computeFilteredKeys(sourceTree, (n) => n.label.toLowerCase().includes(treeSearch))
|
|
457
|
-
: { matched: new Set<TransferKey>(), expand: new Set<TransferKey>() },
|
|
458
|
-
);
|
|
459
|
-
const searchExpand = $derived(treeFiltered.expand);
|
|
460
|
-
const searchMatched = $derived(treeFiltered.matched);
|
|
461
|
-
const effectiveExpanded = $derived(
|
|
462
|
-
treeSearch ? new Set([...baseExpanded, ...searchExpand]) : baseExpanded,
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
const flatNodes = $derived(isTree ? flattenVisible(sourceTree, effectiveExpanded) : []);
|
|
466
|
-
// Hide nodes that (under active search) neither match nor have a matching
|
|
467
|
-
// descendant. A node stays if matched, or is an expanded ancestor of a match.
|
|
468
|
-
const visibleFlat = $derived(
|
|
469
|
-
treeSearch
|
|
470
|
-
? flatNodes.filter(
|
|
471
|
-
(f) => searchMatched.has(f.node.key) || searchExpand.has(f.node.key),
|
|
472
|
-
)
|
|
473
|
-
: flatNodes,
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
// Conduction: derive checked/half from the leaf-level base set.
|
|
477
|
-
const conducted = $derived(
|
|
478
|
-
isTree ? conduct(sourceTree, new Set(leftChecked)) : { checked: new Set<TransferKey>(), half: new Set<TransferKey>() },
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
function isExpanded(key: TransferKey): boolean {
|
|
482
|
-
return effectiveExpanded.has(key);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function toggleExpand(key: TransferKey) {
|
|
486
|
-
const next = new Set(baseExpanded);
|
|
487
|
-
if (next.has(key)) next.delete(key);
|
|
488
|
-
else next.add(key);
|
|
489
|
-
innerExpanded = next;
|
|
490
|
-
expandedTouched = true;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function toggleTreeCheck(node: TreeNodeData) {
|
|
494
|
-
if (disabled || node.disabled) return;
|
|
495
|
-
leftChecked = [...toggleCheck(sourceTree, new Set(leftChecked), node.key)];
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/** Movable (non-disabled, checked) keys within one render group. */
|
|
499
|
-
function groupMovable(
|
|
500
|
-
side: 'left' | 'right',
|
|
501
|
-
group: TransferRenderGroup,
|
|
502
|
-
): TransferKey[] {
|
|
503
|
-
const checked = side === 'left' ? leftChecked : rightChecked;
|
|
504
|
-
return group.items.filter((i) => !i.disabled && checked.includes(i.key)).map((i) => i.key);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** Tri-state for a group's header checkbox. */
|
|
508
|
-
function groupCheckState(
|
|
509
|
-
side: 'left' | 'right',
|
|
510
|
-
group: TransferRenderGroup,
|
|
511
|
-
): { checked: boolean; indeterminate: boolean } {
|
|
512
|
-
const checked = side === 'left' ? leftChecked : rightChecked;
|
|
513
|
-
const selectable = group.items.filter((i) => !disabled && !i.disabled);
|
|
514
|
-
if (selectable.length === 0) return { checked: false, indeterminate: false };
|
|
515
|
-
const picked = selectable.filter((i) => checked.includes(i.key)).length;
|
|
516
|
-
return { checked: picked === selectable.length, indeterminate: picked > 0 && picked < selectable.length };
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function toggleGroup(side: 'left' | 'right', group: TransferRenderGroup) {
|
|
520
|
-
if (disabled) return;
|
|
521
|
-
const keys = group.items.filter((i) => !i.disabled).map((i) => i.key);
|
|
522
|
-
if (keys.length === 0) return;
|
|
523
|
-
const checked = side === 'left' ? leftChecked : rightChecked;
|
|
524
|
-
const allOn = keys.every((k) => checked.includes(k));
|
|
525
|
-
const set = new Set(checked);
|
|
526
|
-
if (allOn) keys.forEach((k) => set.delete(k));
|
|
527
|
-
else keys.forEach((k) => set.add(k));
|
|
528
|
-
if (side === 'left') leftChecked = [...set];
|
|
529
|
-
else rightChecked = [...set];
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function commit(next: TransferKey[]) {
|
|
533
|
-
if (!isControlled) inner = next;
|
|
534
|
-
onChange?.(next);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function toggleChecked(side: 'left' | 'right', key: TransferKey) {
|
|
538
|
-
const list = side === 'left' ? leftVisible : rightVisible;
|
|
539
|
-
const item = list.find((i) => i.key === key);
|
|
540
|
-
if (side === 'left') {
|
|
541
|
-
const wasChecked = leftChecked.includes(key);
|
|
542
|
-
leftChecked = wasChecked
|
|
543
|
-
? leftChecked.filter((k) => k !== key)
|
|
544
|
-
: [...leftChecked, key];
|
|
545
|
-
if (item) {
|
|
546
|
-
if (wasChecked) onDeselect?.(item);
|
|
547
|
-
else onSelect?.(item);
|
|
548
|
-
}
|
|
549
|
-
} else {
|
|
550
|
-
const wasChecked = rightChecked.includes(key);
|
|
551
|
-
rightChecked = wasChecked
|
|
552
|
-
? rightChecked.filter((k) => k !== key)
|
|
553
|
-
: [...rightChecked, key];
|
|
554
|
-
if (item) {
|
|
555
|
-
if (wasChecked) onDeselect?.(item);
|
|
556
|
-
else onSelect?.(item);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function moveToRight() {
|
|
562
|
-
if (disabled) return;
|
|
563
|
-
// In tree mode `leftChecked` is leaf-level; keep only enabled, not-yet-moved.
|
|
564
|
-
const movable = leftItems
|
|
565
|
-
.filter((i) => !i.disabled && leftChecked.includes(i.key))
|
|
566
|
-
.map((i) => i.key);
|
|
567
|
-
if (movable.length === 0) return;
|
|
568
|
-
commit([...current, ...movable]);
|
|
569
|
-
leftChecked = leftChecked.filter((k) => !movable.includes(k));
|
|
570
|
-
// Focus retention (spec §6):移动后焦点留在 SOURCE 列相邻选项 + live 播报。
|
|
571
|
-
const count = movable.length;
|
|
572
|
-
const oldKeys = leftRowKeys;
|
|
573
|
-
const firstMovedIdx = oldKeys.findIndex((k) => movable.includes(k));
|
|
574
|
-
announcer.announce(loc().t('Transfer.movedToRight', { count }));
|
|
575
|
-
void tick().then(() => {
|
|
576
|
-
const keys = leftRowKeys;
|
|
577
|
-
if (keys.length === 0) return;
|
|
578
|
-
const idx = Math.min(Math.max(firstMovedIdx, 0), keys.length - 1);
|
|
579
|
-
const k = keys[idx];
|
|
580
|
-
if (k == null) return;
|
|
581
|
-
leftFocusKey = k;
|
|
582
|
-
leftAnchor = idx;
|
|
583
|
-
focusRow('left', k);
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function moveToLeft() {
|
|
588
|
-
if (disabled) return;
|
|
589
|
-
const movable = rightItems
|
|
590
|
-
.filter((i) => !i.disabled && rightChecked.includes(i.key))
|
|
591
|
-
.map((i) => i.key);
|
|
592
|
-
if (movable.length === 0) return;
|
|
593
|
-
const remove = new Set(movable);
|
|
594
|
-
const count = movable.length;
|
|
595
|
-
const oldKeys = rightRowKeys;
|
|
596
|
-
const firstMovedIdx = oldKeys.findIndex((k) => movable.includes(k));
|
|
597
|
-
commit(current.filter((k) => !remove.has(k)));
|
|
598
|
-
rightChecked = [];
|
|
599
|
-
// Focus retention (spec §6):移动后焦点留在 SOURCE(右)列相邻选项 + live 播报。
|
|
600
|
-
announcer.announce(loc().t('Transfer.movedToLeft', { count }));
|
|
601
|
-
void tick().then(() => {
|
|
602
|
-
const keys = rightRowKeys;
|
|
603
|
-
if (keys.length === 0) return;
|
|
604
|
-
const idx = Math.min(Math.max(firstMovedIdx, 0), keys.length - 1);
|
|
605
|
-
const k = keys[idx];
|
|
606
|
-
if (k == null) return;
|
|
607
|
-
rightFocusKey = k;
|
|
608
|
-
rightAnchor = idx;
|
|
609
|
-
focusRow('right', k);
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/** oneWay: remove a single target item back to the source. */
|
|
614
|
-
function removeOne(key: TransferKey) {
|
|
615
|
-
if (disabled) return;
|
|
616
|
-
commit(current.filter((k) => k !== key));
|
|
617
|
-
rightChecked = rightChecked.filter((k) => k !== key);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// --- remote 搜索防抖(命令式定时器 + cleanup,红线 #3)对齐 Select ---
|
|
621
|
-
let leftTimer: ReturnType<typeof setTimeout> | undefined;
|
|
622
|
-
let rightTimer: ReturnType<typeof setTimeout> | undefined;
|
|
623
|
-
function scheduleSearch(direction: 'left' | 'right', q: string) {
|
|
624
|
-
const timer = direction === 'left' ? leftTimer : rightTimer;
|
|
625
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
626
|
-
const id = setTimeout(() => {
|
|
627
|
-
if (direction === 'left') leftTimer = undefined;
|
|
628
|
-
else rightTimer = undefined;
|
|
629
|
-
onSearch?.(q, direction);
|
|
630
|
-
}, Math.max(0, searchDebounce));
|
|
631
|
-
if (direction === 'left') leftTimer = id;
|
|
632
|
-
else rightTimer = id;
|
|
633
|
-
}
|
|
634
|
-
function onQueryInput(direction: 'left' | 'right', v: string) {
|
|
635
|
-
if (direction === 'left') leftQuery = v;
|
|
636
|
-
else rightQuery = v;
|
|
637
|
-
if (isRemote) scheduleSearch(direction, v);
|
|
638
|
-
}
|
|
639
|
-
// 卸载兜底清理。
|
|
640
|
-
$effect(() => () => {
|
|
641
|
-
if (leftTimer !== undefined) clearTimeout(leftTimer);
|
|
642
|
-
if (rightTimer !== undefined) clearTimeout(rightTimer);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// --- 目标列拖拽重排(HTML5 DnD,与 TagInput 对齐)---------------------------
|
|
646
|
-
// 受控不改 value,仅经 onChange 通知新顺序(红线 #1);拖拽态用 $state,事件
|
|
647
|
-
// 处理命令式 + drop/dragend 清理(红线 #3);重排交由 reorder.ts 纯函数(红线 #2)。
|
|
648
|
-
// 拖拽作用于「当前 target keys 的顺序」,按可见目标项的全局索引映射回 current。
|
|
649
|
-
const canDrag = $derived(draggable && !disabled);
|
|
650
|
-
let dragIndex = $state<number | null>(null); // 被拖拽目标项在 current 中的下标
|
|
651
|
-
let dropIndex = $state<number | null>(null); // 当前悬停目标项下标
|
|
652
|
-
let dropSide = $state<InsertSide | null>(null); // 插入到目标前/后
|
|
653
|
-
|
|
654
|
-
function resetDrag() {
|
|
655
|
-
dragIndex = null;
|
|
656
|
-
dropIndex = null;
|
|
657
|
-
dropSide = null;
|
|
658
|
-
}
|
|
659
|
-
// 把可见目标项映射回它在 current(已选顺序)中的下标。
|
|
660
|
-
function targetIndexOf(key: TransferKey): number {
|
|
661
|
-
return current.indexOf(key);
|
|
662
|
-
}
|
|
663
|
-
function onRowDragStart(e: DragEvent, key: TransferKey) {
|
|
664
|
-
if (!canDrag) {
|
|
665
|
-
e.preventDefault();
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
dragIndex = targetIndexOf(key);
|
|
669
|
-
if (e.dataTransfer) {
|
|
670
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
671
|
-
// 必须 setData,否则部分浏览器不触发 drop。
|
|
672
|
-
e.dataTransfer.setData('text/plain', String(dragIndex));
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
function onRowDragOver(e: DragEvent, key: TransferKey) {
|
|
676
|
-
if (dragIndex === null) return;
|
|
677
|
-
e.preventDefault(); // dragover 必须 preventDefault 才能触发 drop。
|
|
678
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
679
|
-
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
680
|
-
dropIndex = targetIndexOf(key);
|
|
681
|
-
dropSide = computeInsertSide(e.clientY - rect.top, rect.height);
|
|
682
|
-
}
|
|
683
|
-
function onRowDragLeave(e: DragEvent, key: TransferKey) {
|
|
684
|
-
const related = e.relatedTarget as Node | null;
|
|
685
|
-
const cur = e.currentTarget as HTMLElement;
|
|
686
|
-
if (related && cur.contains(related)) return;
|
|
687
|
-
if (dropIndex === targetIndexOf(key)) {
|
|
688
|
-
dropIndex = null;
|
|
689
|
-
dropSide = null;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
function onRowDrop(e: DragEvent, key: TransferKey) {
|
|
693
|
-
if (dragIndex === null || dropSide === null) {
|
|
694
|
-
resetDrag();
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
e.preventDefault();
|
|
698
|
-
const next = reorder(current, dragIndex, targetIndexOf(key), dropSide);
|
|
699
|
-
const changed = next.length === current.length && next.some((k, i) => k !== current[i]);
|
|
700
|
-
resetDrag();
|
|
701
|
-
if (changed) commit(next); // 受控仅 onChange(红线 #1)。
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// --- 虚拟化(仅平铺、非分组面板生效;tree/group 回退全量渲染)--------------
|
|
705
|
-
// 视口=列表容器自身滚动;scrollTop 由命令式 rAF 节流回调写本地 $state,可见区间
|
|
706
|
-
// 纯 $derived render 期只读不读 DOM(红线 #2/#3)。对齐 Select 虚拟化范式。
|
|
707
|
-
const VIRTUAL_OVERSCAN = 4;
|
|
708
|
-
const vItemHeight = $derived(itemHeight > 0 ? itemHeight : 32);
|
|
709
|
-
const leftVirtual = $derived(virtualize && !grouped && !isTree);
|
|
710
|
-
const rightVirtual = $derived(virtualize && !grouped);
|
|
711
|
-
let leftScrollTop = $state(0);
|
|
712
|
-
let rightScrollTop = $state(0);
|
|
713
|
-
let leftViewportH = $state(0);
|
|
714
|
-
let rightViewportH = $state(0);
|
|
715
|
-
|
|
716
|
-
const leftVTotal = $derived(leftVisible.length * vItemHeight);
|
|
717
|
-
const rightVTotal = $derived(rightVisible.length * vItemHeight);
|
|
718
|
-
const leftVRange = $derived(
|
|
719
|
-
leftVirtual
|
|
720
|
-
? fixedRange(leftScrollTop, leftViewportH, vItemHeight, leftVisible.length, VIRTUAL_OVERSCAN)
|
|
721
|
-
: { startIndex: 0, endIndex: leftVisible.length },
|
|
722
|
-
);
|
|
723
|
-
const rightVRange = $derived(
|
|
724
|
-
rightVirtual
|
|
725
|
-
? fixedRange(rightScrollTop, rightViewportH, vItemHeight, rightVisible.length, VIRTUAL_OVERSCAN)
|
|
726
|
-
: { startIndex: 0, endIndex: rightVisible.length },
|
|
727
|
-
);
|
|
728
|
-
const leftVItems = $derived(
|
|
729
|
-
leftVirtual ? leftVisible.slice(leftVRange.startIndex, leftVRange.endIndex) : leftVisible,
|
|
730
|
-
);
|
|
731
|
-
const rightVItems = $derived(
|
|
732
|
-
rightVirtual ? rightVisible.slice(rightVRange.startIndex, rightVRange.endIndex) : rightVisible,
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
// 滚动容器:用 attachment 绑定列表元素,命令式 rAF 节流监听 scroll,把视口高度与
|
|
736
|
-
// scrollTop 写回本地 $state 驱动 vRange 派生;attachment 返回 cleanup(红线 #3)。
|
|
737
|
-
function virtualScroll(
|
|
738
|
-
setTop: (n: number) => void,
|
|
739
|
-
setViewport: (n: number) => void,
|
|
740
|
-
): Attachment<HTMLUListElement> {
|
|
741
|
-
return (el) => {
|
|
742
|
-
let raf = 0;
|
|
743
|
-
setViewport(el.clientHeight);
|
|
744
|
-
setTop(el.scrollTop);
|
|
745
|
-
function onScroll() {
|
|
746
|
-
if (raf) return;
|
|
747
|
-
raf = requestAnimationFrame(() => {
|
|
748
|
-
raf = 0;
|
|
749
|
-
setTop(el.scrollTop);
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
el.addEventListener('scroll', onScroll, { passive: true });
|
|
753
|
-
return () => {
|
|
754
|
-
el.removeEventListener('scroll', onScroll);
|
|
755
|
-
if (raf) cancelAnimationFrame(raf);
|
|
756
|
-
};
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
const noopAttach: Attachment<HTMLUListElement> = () => {};
|
|
760
|
-
const leftScrollAttach = $derived(
|
|
761
|
-
leftVirtual
|
|
762
|
-
? virtualScroll(
|
|
763
|
-
(n) => (leftScrollTop = n),
|
|
764
|
-
(n) => (leftViewportH = n),
|
|
765
|
-
)
|
|
766
|
-
: noopAttach,
|
|
767
|
-
);
|
|
768
|
-
const rightScrollAttach = $derived(
|
|
769
|
-
rightVirtual
|
|
770
|
-
? virtualScroll(
|
|
771
|
-
(n) => (rightScrollTop = n),
|
|
772
|
-
(n) => (rightViewportH = n),
|
|
773
|
-
)
|
|
774
|
-
: noopAttach,
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
// --- showPath: build label path for tree leaves in right panel ---------------
|
|
778
|
-
// Map leaf key → ancestor-path label string (e.g. "Parent / Child").
|
|
779
|
-
function buildPathMap(
|
|
780
|
-
nodes: TransferTreeNode[],
|
|
781
|
-
ancestors: string[] = [],
|
|
782
|
-
): Map<TransferKey, string> {
|
|
783
|
-
const map = new Map<TransferKey, string>();
|
|
784
|
-
for (const node of nodes) {
|
|
785
|
-
const path = [...ancestors, node.label];
|
|
786
|
-
if (node.children && node.children.length > 0) {
|
|
787
|
-
const childMap = buildPathMap(node.children, path);
|
|
788
|
-
childMap.forEach((v, k) => map.set(k, v));
|
|
789
|
-
} else {
|
|
790
|
-
map.set(node.key, path.join(' / '));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return map;
|
|
794
|
-
}
|
|
795
|
-
const treePathMap = $derived(isTree && showPath ? buildPathMap(treeData) : new Map<TransferKey, string>());
|
|
796
|
-
|
|
797
|
-
function itemDisplayLabel(item: TransferItem, side: 'left' | 'right'): string {
|
|
798
|
-
if (side === 'right' && isTree && showPath) {
|
|
799
|
-
return treePathMap.get(item.key) ?? item.label;
|
|
800
|
-
}
|
|
801
|
-
return item.label;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// --- Pagination (left panel, list/groupList only) -------------------------
|
|
805
|
-
// Use a plain getter to extract defaultCurrentPage once at init time, avoiding
|
|
806
|
-
// the "state_referenced_locally" reactive-capture warning.
|
|
807
|
-
function getInitialPage(): number {
|
|
808
|
-
return pagination?.defaultCurrentPage ?? 1;
|
|
809
|
-
}
|
|
810
|
-
let paginationPage = $state(getInitialPage());
|
|
811
|
-
const isControlledPage = $derived(pagination?.currentPage !== undefined);
|
|
812
|
-
const activePage = $derived(isControlledPage ? (pagination!.currentPage ?? 1) : paginationPage);
|
|
813
|
-
const pageSize = $derived(pagination?.pageSize ?? 0);
|
|
814
|
-
const hasPagination = $derived(!!pagination && pageSize > 0 && !isTree);
|
|
815
|
-
|
|
816
|
-
const leftVisiblePaged = $derived(
|
|
817
|
-
hasPagination
|
|
818
|
-
? leftVisible.slice((activePage - 1) * pageSize, activePage * pageSize)
|
|
819
|
-
: leftVisible,
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
function onPageChange(page: number) {
|
|
823
|
-
if (!isControlledPage) paginationPage = page;
|
|
824
|
-
pagination?.onPageChange?.(page);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const moveRightDisabled = $derived(
|
|
828
|
-
disabled ||
|
|
829
|
-
leftItems.filter((i) => !i.disabled && leftChecked.includes(i.key)).length === 0,
|
|
830
|
-
);
|
|
831
|
-
const moveLeftDisabled = $derived(
|
|
832
|
-
disabled ||
|
|
833
|
-
rightItems.filter((i) => !i.disabled && rightChecked.includes(i.key)).length === 0,
|
|
834
|
-
);
|
|
835
|
-
|
|
836
|
-
const cls = $derived(
|
|
837
|
-
['cd-transfer', `cd-transfer--${size}`, `cd-transfer--${status}`, disabled && 'cd-transfer--disabled']
|
|
838
|
-
.filter(Boolean)
|
|
839
|
-
.join(' '),
|
|
840
|
-
);
|
|
841
|
-
</script>
|
|
842
|
-
|
|
843
|
-
{#snippet itemRow(side: 'left' | 'right', item: TransferItem, style = '', asOption = false)}
|
|
844
|
-
{@const tIndex = side === 'right' ? targetIndexOf(item.key) : -1}
|
|
845
|
-
{@const dragRow = side === 'right' && canDrag && !(item.disabled ?? false)}
|
|
846
|
-
{@const isChecked = (side === 'left' ? leftChecked : rightChecked).includes(item.key)}
|
|
847
|
-
{@const rowDisabled = disabled || (item.disabled ?? false)}
|
|
848
|
-
{@const displayLabel = itemDisplayLabel(item, side)}
|
|
849
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
850
|
-
<li
|
|
851
|
-
class="cd-transfer__item"
|
|
852
|
-
class:cd-transfer__item--draggable={dragRow}
|
|
853
|
-
class:cd-transfer__item--dragging={side === 'right' && dragIndex !== null && dragIndex === tIndex}
|
|
854
|
-
class:cd-transfer__item--drop-before={side === 'right' &&
|
|
855
|
-
dropIndex === tIndex &&
|
|
856
|
-
dropSide === 'before'}
|
|
857
|
-
class:cd-transfer__item--drop-after={side === 'right' &&
|
|
858
|
-
dropIndex === tIndex &&
|
|
859
|
-
dropSide === 'after'}
|
|
860
|
-
{style}
|
|
861
|
-
role={asOption ? 'option' : undefined}
|
|
862
|
-
aria-label={asOption ? displayLabel : undefined}
|
|
863
|
-
aria-selected={asOption ? (side === 'right' && oneWay ? true : isChecked) : undefined}
|
|
864
|
-
aria-disabled={asOption ? rowDisabled || undefined : undefined}
|
|
865
|
-
data-transfer-key={asOption ? item.key : undefined}
|
|
866
|
-
tabindex={asOption ? rowTabindex(side, item.key) : undefined}
|
|
867
|
-
onkeydown={asOption ? (e) => onRowKeydown(e, side, item.key) : undefined}
|
|
868
|
-
onfocus={asOption ? () => onRowFocus(side, item.key) : undefined}
|
|
869
|
-
onclick={asOption && !(side === 'right' && oneWay)
|
|
870
|
-
? (e) => onRowOptionClick(e, side, item)
|
|
871
|
-
: undefined}
|
|
872
|
-
draggable={dragRow}
|
|
873
|
-
ondragstart={dragRow ? (e) => onRowDragStart(e, item.key) : undefined}
|
|
874
|
-
ondragover={side === 'right' && canDrag ? (e) => onRowDragOver(e, item.key) : undefined}
|
|
875
|
-
ondragleave={side === 'right' && canDrag ? (e) => onRowDragLeave(e, item.key) : undefined}
|
|
876
|
-
ondrop={side === 'right' && canDrag ? (e) => onRowDrop(e, item.key) : undefined}
|
|
877
|
-
ondragend={side === 'right' && canDrag ? resetDrag : undefined}
|
|
878
|
-
>
|
|
879
|
-
{#if side === 'right' && oneWay}
|
|
880
|
-
{#if renderSelectedItem}
|
|
881
|
-
{@render renderSelectedItem({ item, onRemove: () => removeOne(item.key) })}
|
|
882
|
-
{:else}
|
|
883
|
-
<span class="cd-transfer__item-label">{displayLabel}</span>
|
|
884
|
-
<button
|
|
885
|
-
type="button"
|
|
886
|
-
class="cd-transfer__remove"
|
|
887
|
-
aria-label={loc().t('Transfer.remove')}
|
|
888
|
-
disabled={disabled || (item.disabled ?? false)}
|
|
889
|
-
onclick={() => removeOne(item.key)}
|
|
890
|
-
>
|
|
891
|
-
<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true" focusable="false">
|
|
892
|
-
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M4 4l8 8M12 4l-8 8" />
|
|
893
|
-
</svg>
|
|
894
|
-
</button>
|
|
895
|
-
{/if}
|
|
896
|
-
{:else if asOption}
|
|
897
|
-
{#if side === 'right' && renderSelectedItem}
|
|
898
|
-
{@render renderSelectedItem({ item, onRemove: () => removeOne(item.key) })}
|
|
899
|
-
{:else if side === 'left' && renderSourceItem}
|
|
900
|
-
{@render renderSourceItem({ item, onChange: () => toggleChecked(side, item.key), checked: isChecked })}
|
|
901
|
-
{:else}
|
|
902
|
-
<!-- 选项行:单一 Tab 停靠点(行本身可聚焦),勾选框为纯视觉,无 input/无第二停靠点。 -->
|
|
903
|
-
<span class="cd-transfer__option-control" aria-hidden="true">
|
|
904
|
-
<span class="cd-transfer__check" class:cd-transfer__check--on={isChecked}>
|
|
905
|
-
{#if isChecked}
|
|
906
|
-
<svg viewBox="0 0 16 16" width="10" height="10" focusable="false">
|
|
907
|
-
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M3.5 8.5 6.5 11.5 12.5 4.5" />
|
|
908
|
-
</svg>
|
|
909
|
-
{/if}
|
|
910
|
-
</span>
|
|
911
|
-
<span class="cd-transfer__option-label">{displayLabel}</span>
|
|
912
|
-
</span>
|
|
913
|
-
{/if}
|
|
914
|
-
{:else}
|
|
915
|
-
{#if side === 'left' && renderSourceItem}
|
|
916
|
-
{@render renderSourceItem({ item, onChange: () => toggleChecked(side, item.key), checked: isChecked })}
|
|
917
|
-
{:else}
|
|
918
|
-
<Checkbox
|
|
919
|
-
{size}
|
|
920
|
-
checked={isChecked}
|
|
921
|
-
disabled={disabled || (item.disabled ?? false)}
|
|
922
|
-
onChange={() => toggleChecked(side, item.key)}
|
|
923
|
-
>
|
|
924
|
-
{displayLabel}
|
|
925
|
-
</Checkbox>
|
|
926
|
-
{/if}
|
|
927
|
-
{/if}
|
|
928
|
-
</li>
|
|
929
|
-
{/snippet}
|
|
930
|
-
|
|
931
|
-
{#snippet treeRow(f: FlatNode)}
|
|
932
|
-
{@const node = f.node}
|
|
933
|
-
{@const checked = conducted.checked.has(node.key)}
|
|
934
|
-
{@const half = !checked && conducted.half.has(node.key)}
|
|
935
|
-
<li
|
|
936
|
-
class="cd-transfer__tree-node"
|
|
937
|
-
style="padding-inline-start: calc({f.level} * var(--cd-transfer-tree-indent, 18px))"
|
|
938
|
-
>
|
|
939
|
-
{#if f.hasChildren}
|
|
940
|
-
<button
|
|
941
|
-
type="button"
|
|
942
|
-
class="cd-transfer__switcher"
|
|
943
|
-
class:cd-transfer__switcher--open={isExpanded(node.key)}
|
|
944
|
-
aria-label={isExpanded(node.key) ? loc().t('Tree.collapse') : loc().t('Tree.expand')}
|
|
945
|
-
onclick={() => toggleExpand(node.key)}
|
|
946
|
-
>
|
|
947
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" focusable="false">
|
|
948
|
-
<path fill="currentColor" d="M6 4l4 4-4 4V4Z" />
|
|
949
|
-
</svg>
|
|
950
|
-
</button>
|
|
951
|
-
{:else}
|
|
952
|
-
<span class="cd-transfer__switcher cd-transfer__switcher--leaf" aria-hidden="true"></span>
|
|
953
|
-
{/if}
|
|
954
|
-
<Checkbox
|
|
955
|
-
{size}
|
|
956
|
-
{checked}
|
|
957
|
-
indeterminate={half}
|
|
958
|
-
disabled={disabled || (node.disabled ?? false)}
|
|
959
|
-
onChange={() => toggleTreeCheck(node)}
|
|
960
|
-
>
|
|
961
|
-
{node.label}
|
|
962
|
-
</Checkbox>
|
|
963
|
-
</li>
|
|
964
|
-
{/snippet}
|
|
965
|
-
|
|
966
|
-
{#snippet emptyNode(side: 'left' | 'right', isSearch = false)}
|
|
967
|
-
{@const customEmpty = isSearch ? emptyContent?.search : (side === 'left' ? emptyContent?.left : emptyContent?.right)}
|
|
968
|
-
<li class="cd-transfer__empty">
|
|
969
|
-
{#if customEmpty}
|
|
970
|
-
{#if typeof customEmpty === 'string'}
|
|
971
|
-
{customEmpty}
|
|
972
|
-
{:else}
|
|
973
|
-
{@render customEmpty()}
|
|
974
|
-
{/if}
|
|
975
|
-
{:else}
|
|
976
|
-
{isSearch ? loc().t('Transfer.searchEmpty') : loc().t('Transfer.empty')}
|
|
977
|
-
{/if}
|
|
978
|
-
</li>
|
|
979
|
-
{/snippet}
|
|
980
|
-
|
|
981
|
-
{#snippet panelList(side: 'left' | 'right', visible: TransferItem[], groups: TransferRenderGroup[])}
|
|
982
|
-
{@const isVirtual = side === 'left' ? leftVirtual : rightVirtual}
|
|
983
|
-
{@const vItems = side === 'left' ? leftVItems : rightVItems}
|
|
984
|
-
{@const vRange = side === 'left' ? leftVRange : rightVRange}
|
|
985
|
-
{@const vTotal = side === 'left' ? leftVTotal : rightVTotal}
|
|
986
|
-
<!-- For left panel with pagination, use paged slice. -->
|
|
987
|
-
{@const displayVisible = side === 'left' && hasPagination ? leftVisiblePaged : visible}
|
|
988
|
-
<!-- empty state: search-filtered vs truly empty -->
|
|
989
|
-
{@const isSearchFiltered = side === 'left' ? (leftQuery.trim() !== '') : (rightQuery.trim() !== '')}
|
|
990
|
-
<!-- 空态(仅含 cd-transfer__empty 占位 li)时 listbox 无 option 子节点 → 违反
|
|
991
|
-
aria-required-children;空态下退化为普通容器(无 role/listbox ARIA),按 APG
|
|
992
|
-
处理空 listbox。loading 时仍非空(spinner li)但同样不是 option,故并入判定。 -->
|
|
993
|
-
{@const isEmptyPanel = !loading && visible.length === 0}
|
|
994
|
-
<ul
|
|
995
|
-
class="cd-transfer__list"
|
|
996
|
-
role={isEmptyPanel ? undefined : 'listbox'}
|
|
997
|
-
aria-multiselectable={isEmptyPanel ? undefined : 'true'}
|
|
998
|
-
aria-labelledby={!isEmptyPanel && showPanelTitle
|
|
999
|
-
? (side === 'left' ? leftTitleId : rightTitleId)
|
|
1000
|
-
: undefined}
|
|
1001
|
-
aria-label={!isEmptyPanel && !showPanelTitle
|
|
1002
|
-
? (side === 'left' ? sourceFallback : targetFallback)
|
|
1003
|
-
: undefined}
|
|
1004
|
-
aria-disabled={disabled || undefined}
|
|
1005
|
-
{@attach captureList(side)}
|
|
1006
|
-
{@attach side === 'left' ? leftScrollAttach : rightScrollAttach}
|
|
1007
|
-
>
|
|
1008
|
-
{#if loading}
|
|
1009
|
-
<li class="cd-transfer__loading" aria-live="polite">
|
|
1010
|
-
<span class="cd-transfer__spinner" aria-hidden="true"></span>
|
|
1011
|
-
<span>{loc().t('Transfer.loading')}</span>
|
|
1012
|
-
</li>
|
|
1013
|
-
{/if}
|
|
1014
|
-
{#if side === 'left' && isTree}
|
|
1015
|
-
{#each visibleFlat as f (f.node.key)}
|
|
1016
|
-
{@render treeRow(f)}
|
|
1017
|
-
{:else}
|
|
1018
|
-
{#if !loading}
|
|
1019
|
-
{@render emptyNode(side, isSearchFiltered)}
|
|
1020
|
-
{/if}
|
|
1021
|
-
{/each}
|
|
1022
|
-
{:else if grouped}
|
|
1023
|
-
{#each groups as group (group.title)}
|
|
1024
|
-
{@const state = groupCheckState(side, group)}
|
|
1025
|
-
<li class="cd-transfer__group">
|
|
1026
|
-
<div class="cd-transfer__group-header">
|
|
1027
|
-
<Checkbox
|
|
1028
|
-
{size}
|
|
1029
|
-
checked={state.checked}
|
|
1030
|
-
indeterminate={state.indeterminate}
|
|
1031
|
-
disabled={disabled || group.items.every((i) => i.disabled)}
|
|
1032
|
-
onChange={() => toggleGroup(side, group)}
|
|
1033
|
-
>
|
|
1034
|
-
{group.title}
|
|
1035
|
-
</Checkbox>
|
|
1036
|
-
<span class="cd-transfer__group-count">{group.items.length}</span>
|
|
1037
|
-
</div>
|
|
1038
|
-
<ul class="cd-transfer__group-list">
|
|
1039
|
-
{#each group.items as item (item.key)}
|
|
1040
|
-
{@render itemRow(side, item)}
|
|
1041
|
-
{/each}
|
|
1042
|
-
</ul>
|
|
1043
|
-
</li>
|
|
1044
|
-
{:else}
|
|
1045
|
-
{#if !loading}
|
|
1046
|
-
{@render emptyNode(side, isSearchFiltered)}
|
|
1047
|
-
{/if}
|
|
1048
|
-
{/each}
|
|
1049
|
-
{:else if isVirtual}
|
|
1050
|
-
<!-- 虚拟化:spacer 撑总高,可见项绝对定位按全局索引偏移;只渲染视口切片 -->
|
|
1051
|
-
<li class="cd-transfer__spacer" style={`block-size:${vTotal}px`} aria-hidden="true"></li>
|
|
1052
|
-
{#each vItems as item, i (item.key)}
|
|
1053
|
-
{@render itemRow(
|
|
1054
|
-
side,
|
|
1055
|
-
item,
|
|
1056
|
-
`position:absolute; inset-inline:0; transform:translateY(${(vRange.startIndex + i) * vItemHeight}px); block-size:${vItemHeight}px`,
|
|
1057
|
-
true,
|
|
1058
|
-
)}
|
|
1059
|
-
{/each}
|
|
1060
|
-
{#if visible.length === 0 && !loading}
|
|
1061
|
-
{@render emptyNode(side, isSearchFiltered)}
|
|
1062
|
-
{/if}
|
|
1063
|
-
{:else}
|
|
1064
|
-
{#each displayVisible as item (item.key)}
|
|
1065
|
-
{@render itemRow(side, item, '', true)}
|
|
1066
|
-
{:else}
|
|
1067
|
-
{#if !loading}
|
|
1068
|
-
{@render emptyNode(side, isSearchFiltered)}
|
|
1069
|
-
{/if}
|
|
1070
|
-
{/each}
|
|
1071
|
-
{/if}
|
|
1072
|
-
</ul>
|
|
1073
|
-
{/snippet}
|
|
1074
|
-
|
|
1075
|
-
<div class={cls} role="group" aria-disabled={disabled || undefined}>
|
|
1076
|
-
<div class="cd-transfer__panel">
|
|
1077
|
-
{#if renderSourcePanel}
|
|
1078
|
-
{@render renderSourcePanel({
|
|
1079
|
-
dataSource: leftItems,
|
|
1080
|
-
selectedItems: rightItems,
|
|
1081
|
-
onChange: commit,
|
|
1082
|
-
onSearch: (v) => onQueryInput('left', v),
|
|
1083
|
-
inputValue: leftQuery,
|
|
1084
|
-
searchResult: leftVisible,
|
|
1085
|
-
})}
|
|
1086
|
-
{:else}
|
|
1087
|
-
{#if showPanelTitle}
|
|
1088
|
-
<div class="cd-transfer__panel-header">
|
|
1089
|
-
{#if renderSourceHeader}
|
|
1090
|
-
{@render renderSourceHeader({
|
|
1091
|
-
totalContent: loc().t('Transfer.itemsUnit', { count: leftItems.length }),
|
|
1092
|
-
allContent: loc().t('Transfer.itemsUnit', { count: items.length }),
|
|
1093
|
-
onAllClick: () => {},
|
|
1094
|
-
title: titles?.[0] ?? loc().t('Transfer.titleSource'),
|
|
1095
|
-
allChecked: leftItems.length > 0 && leftItems.filter((i) => !i.disabled).every((i) => leftChecked.includes(i.key)),
|
|
1096
|
-
showButton: true,
|
|
1097
|
-
leafOnlyNum: isTree ? leftVisible.filter((i) => !current.includes(i.key)).length : undefined,
|
|
1098
|
-
})}
|
|
1099
|
-
{:else}
|
|
1100
|
-
<span id={leftTitleId} class="cd-transfer__panel-title">{titles?.[0] ?? loc().t('Transfer.titleSource')}</span>
|
|
1101
|
-
<span class="cd-transfer__panel-count">{loc().t('Transfer.itemsUnit', { count: leftItems.length })}</span>
|
|
1102
|
-
{/if}
|
|
1103
|
-
</div>
|
|
1104
|
-
{/if}
|
|
1105
|
-
{#if filter}
|
|
1106
|
-
<div class="cd-transfer__panel-search">
|
|
1107
|
-
<Input
|
|
1108
|
-
{size}
|
|
1109
|
-
value={leftQuery}
|
|
1110
|
-
placeholder={searchPlaceholderText}
|
|
1111
|
-
clearable
|
|
1112
|
-
disabled={disabled}
|
|
1113
|
-
ariaLabel={searchPlaceholderText}
|
|
1114
|
-
onInput={(v) => onQueryInput('left', v)}
|
|
1115
|
-
{...(inputProps ?? {})}
|
|
1116
|
-
/>
|
|
1117
|
-
</div>
|
|
1118
|
-
{/if}
|
|
1119
|
-
{@render panelList('left', leftVisible, leftGroups)}
|
|
1120
|
-
{#if hasPagination}
|
|
1121
|
-
<div class="cd-transfer__pagination">
|
|
1122
|
-
<Pagination
|
|
1123
|
-
total={leftVisible.length}
|
|
1124
|
-
currentPage={activePage}
|
|
1125
|
-
pageSize={pageSize}
|
|
1126
|
-
onChange={(page) => onPageChange(page)}
|
|
1127
|
-
size="small"
|
|
1128
|
-
/>
|
|
1129
|
-
</div>
|
|
1130
|
-
{/if}
|
|
1131
|
-
{/if}
|
|
1132
|
-
</div>
|
|
1133
|
-
|
|
1134
|
-
<div class="cd-transfer__ops">
|
|
1135
|
-
<Button
|
|
1136
|
-
type="primary"
|
|
1137
|
-
size="small"
|
|
1138
|
-
ariaLabel={loc().t('Transfer.moveToRight')}
|
|
1139
|
-
disabled={moveRightDisabled}
|
|
1140
|
-
onclick={moveToRight}
|
|
1141
|
-
>
|
|
1142
|
-
>
|
|
1143
|
-
</Button>
|
|
1144
|
-
{#if !oneWay}
|
|
1145
|
-
<Button
|
|
1146
|
-
type="primary"
|
|
1147
|
-
size="small"
|
|
1148
|
-
ariaLabel={loc().t('Transfer.moveToLeft')}
|
|
1149
|
-
disabled={moveLeftDisabled}
|
|
1150
|
-
onclick={moveToLeft}
|
|
1151
|
-
>
|
|
1152
|
-
<
|
|
1153
|
-
</Button>
|
|
1154
|
-
{/if}
|
|
1155
|
-
</div>
|
|
1156
|
-
|
|
1157
|
-
<div class="cd-transfer__panel">
|
|
1158
|
-
{#if renderSelectedPanel}
|
|
1159
|
-
{@render renderSelectedPanel({
|
|
1160
|
-
selectedItems: rightItems,
|
|
1161
|
-
onRemove: removeOne,
|
|
1162
|
-
onSortEnd: commit,
|
|
1163
|
-
})}
|
|
1164
|
-
{:else}
|
|
1165
|
-
{#if showPanelTitle}
|
|
1166
|
-
<div class="cd-transfer__panel-header">
|
|
1167
|
-
{#if renderSelectedHeader}
|
|
1168
|
-
{@render renderSelectedHeader({
|
|
1169
|
-
totalContent: loc().t('Transfer.itemsUnit', { count: rightItems.length }),
|
|
1170
|
-
allContent: loc().t('Transfer.itemsUnit', { count: items.length }),
|
|
1171
|
-
onAllClick: () => {},
|
|
1172
|
-
title: titles?.[1] ?? loc().t('Transfer.titleTarget'),
|
|
1173
|
-
})}
|
|
1174
|
-
{:else}
|
|
1175
|
-
<span id={rightTitleId} class="cd-transfer__panel-title">{titles?.[1] ?? loc().t('Transfer.titleTarget')}</span>
|
|
1176
|
-
<span class="cd-transfer__panel-count">{loc().t('Transfer.itemsUnit', { count: rightItems.length })}</span>
|
|
1177
|
-
{/if}
|
|
1178
|
-
</div>
|
|
1179
|
-
{/if}
|
|
1180
|
-
{#if filter}
|
|
1181
|
-
<div class="cd-transfer__panel-search">
|
|
1182
|
-
<Input
|
|
1183
|
-
{size}
|
|
1184
|
-
value={rightQuery}
|
|
1185
|
-
placeholder={searchPlaceholderText}
|
|
1186
|
-
clearable
|
|
1187
|
-
disabled={disabled}
|
|
1188
|
-
ariaLabel={searchPlaceholderText}
|
|
1189
|
-
onInput={(v) => onQueryInput('right', v)}
|
|
1190
|
-
{...(inputProps ?? {})}
|
|
1191
|
-
/>
|
|
1192
|
-
</div>
|
|
1193
|
-
{/if}
|
|
1194
|
-
{@render panelList('right', rightVisible, rightGroups)}
|
|
1195
|
-
{/if}
|
|
1196
|
-
</div>
|
|
1197
|
-
</div>
|
|
1198
|
-
|
|
1199
|
-
<style>
|
|
1200
|
-
.cd-transfer {
|
|
1201
|
-
display: inline-flex;
|
|
1202
|
-
align-items: stretch;
|
|
1203
|
-
gap: var(--cd-transfer-gap);
|
|
1204
|
-
color: var(--cd-color-text-0);
|
|
1205
|
-
}
|
|
1206
|
-
.cd-transfer--disabled {
|
|
1207
|
-
opacity: 0.6;
|
|
1208
|
-
}
|
|
1209
|
-
.cd-transfer__panel {
|
|
1210
|
-
display: flex;
|
|
1211
|
-
flex-direction: column;
|
|
1212
|
-
inline-size: var(--cd-transfer-panel-width);
|
|
1213
|
-
block-size: var(--cd-transfer-panel-height);
|
|
1214
|
-
border: 1px solid var(--cd-transfer-panel-border);
|
|
1215
|
-
border-radius: var(--cd-transfer-panel-radius);
|
|
1216
|
-
overflow: hidden;
|
|
1217
|
-
}
|
|
1218
|
-
.cd-transfer__panel-header {
|
|
1219
|
-
display: flex;
|
|
1220
|
-
align-items: center;
|
|
1221
|
-
justify-content: space-between;
|
|
1222
|
-
padding-block: var(--cd-spacing-2);
|
|
1223
|
-
padding-inline: var(--cd-spacing-3);
|
|
1224
|
-
background: var(--cd-transfer-panel-header-bg);
|
|
1225
|
-
border-block-end: 1px solid var(--cd-transfer-panel-border);
|
|
1226
|
-
font-weight: var(--cd-font-weight-medium);
|
|
1227
|
-
}
|
|
1228
|
-
.cd-transfer__panel-count {
|
|
1229
|
-
color: var(--cd-color-text-2);
|
|
1230
|
-
font-size: var(--cd-font-size-1);
|
|
1231
|
-
font-weight: var(--cd-font-weight-regular);
|
|
1232
|
-
}
|
|
1233
|
-
.cd-transfer__panel-search {
|
|
1234
|
-
padding: var(--cd-spacing-2);
|
|
1235
|
-
border-block-end: 1px solid var(--cd-transfer-panel-border);
|
|
1236
|
-
}
|
|
1237
|
-
.cd-transfer__list {
|
|
1238
|
-
position: relative;
|
|
1239
|
-
flex: 1 1 auto;
|
|
1240
|
-
margin: 0;
|
|
1241
|
-
padding: 0;
|
|
1242
|
-
list-style: none;
|
|
1243
|
-
overflow-y: auto;
|
|
1244
|
-
}
|
|
1245
|
-
/* 虚拟化占位:撑出总高度,可见项绝对定位浮于其上。 */
|
|
1246
|
-
.cd-transfer__spacer {
|
|
1247
|
-
pointer-events: none;
|
|
1248
|
-
}
|
|
1249
|
-
.cd-transfer__item {
|
|
1250
|
-
display: flex;
|
|
1251
|
-
align-items: center;
|
|
1252
|
-
justify-content: space-between;
|
|
1253
|
-
gap: var(--cd-spacing-2);
|
|
1254
|
-
min-block-size: var(--cd-transfer-item-height);
|
|
1255
|
-
padding-inline: var(--cd-spacing-3);
|
|
1256
|
-
}
|
|
1257
|
-
.cd-transfer__item:hover {
|
|
1258
|
-
background: var(--cd-transfer-item-bg-hover);
|
|
1259
|
-
}
|
|
1260
|
-
.cd-transfer__item--draggable {
|
|
1261
|
-
cursor: grab;
|
|
1262
|
-
}
|
|
1263
|
-
.cd-transfer__item--dragging {
|
|
1264
|
-
opacity: 0.5;
|
|
1265
|
-
}
|
|
1266
|
-
/* 拖拽插入指示线:目标项上/下边缘 2px 主色线。 */
|
|
1267
|
-
.cd-transfer__item--drop-before {
|
|
1268
|
-
box-shadow: inset 0 2px 0 0 var(--cd-color-primary);
|
|
1269
|
-
}
|
|
1270
|
-
.cd-transfer__item--drop-after {
|
|
1271
|
-
box-shadow: inset 0 -2px 0 0 var(--cd-color-primary);
|
|
1272
|
-
}
|
|
1273
|
-
/* 选项行(listbox option):可聚焦、选中/禁用强调、纯视觉勾选框。 */
|
|
1274
|
-
.cd-transfer__item[role='option'] {
|
|
1275
|
-
cursor: pointer;
|
|
1276
|
-
outline: none;
|
|
1277
|
-
}
|
|
1278
|
-
.cd-transfer__item[role='option']:focus-visible {
|
|
1279
|
-
box-shadow: inset 0 0 0 2px var(--cd-color-primary);
|
|
1280
|
-
}
|
|
1281
|
-
.cd-transfer__item[aria-selected='true'] {
|
|
1282
|
-
background: var(--cd-transfer-item-bg-checked, var(--cd-color-primary-light-default));
|
|
1283
|
-
}
|
|
1284
|
-
.cd-transfer__item[aria-disabled='true'] {
|
|
1285
|
-
color: var(--cd-transfer-item-disabled-text, var(--cd-color-text-2));
|
|
1286
|
-
cursor: not-allowed;
|
|
1287
|
-
}
|
|
1288
|
-
.cd-transfer__option-control {
|
|
1289
|
-
display: inline-flex;
|
|
1290
|
-
align-items: center;
|
|
1291
|
-
gap: var(--cd-spacing-2);
|
|
1292
|
-
pointer-events: none;
|
|
1293
|
-
min-inline-size: 0;
|
|
1294
|
-
flex: 1 1 auto;
|
|
1295
|
-
}
|
|
1296
|
-
.cd-transfer__check {
|
|
1297
|
-
display: inline-flex;
|
|
1298
|
-
align-items: center;
|
|
1299
|
-
justify-content: center;
|
|
1300
|
-
flex: none;
|
|
1301
|
-
inline-size: 16px;
|
|
1302
|
-
block-size: 16px;
|
|
1303
|
-
border: 1px solid var(--cd-color-border);
|
|
1304
|
-
border-radius: var(--cd-radius-small, 4px);
|
|
1305
|
-
color: var(--cd-color-bg-0, #fff);
|
|
1306
|
-
background: var(--cd-color-bg-0);
|
|
1307
|
-
}
|
|
1308
|
-
.cd-transfer__check--on {
|
|
1309
|
-
background: var(--cd-color-primary);
|
|
1310
|
-
border-color: var(--cd-color-primary);
|
|
1311
|
-
}
|
|
1312
|
-
.cd-transfer__option-label {
|
|
1313
|
-
overflow: hidden;
|
|
1314
|
-
text-overflow: ellipsis;
|
|
1315
|
-
white-space: nowrap;
|
|
1316
|
-
}
|
|
1317
|
-
.cd-transfer__loading {
|
|
1318
|
-
display: flex;
|
|
1319
|
-
align-items: center;
|
|
1320
|
-
justify-content: center;
|
|
1321
|
-
gap: var(--cd-spacing-2);
|
|
1322
|
-
padding-block: var(--cd-spacing-3);
|
|
1323
|
-
color: var(--cd-color-text-3);
|
|
1324
|
-
font-size: var(--cd-font-size-1);
|
|
1325
|
-
}
|
|
1326
|
-
.cd-transfer__spinner {
|
|
1327
|
-
inline-size: 1em;
|
|
1328
|
-
block-size: 1em;
|
|
1329
|
-
border: 2px solid var(--cd-color-border);
|
|
1330
|
-
border-block-start-color: var(--cd-color-primary);
|
|
1331
|
-
border-radius: var(--cd-radius-full);
|
|
1332
|
-
animation: cd-transfer-spin 0.7s linear infinite;
|
|
1333
|
-
}
|
|
1334
|
-
@keyframes cd-transfer-spin {
|
|
1335
|
-
to {
|
|
1336
|
-
transform: rotate(360deg);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1340
|
-
.cd-transfer__spinner {
|
|
1341
|
-
animation: none;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
.cd-transfer__item-label {
|
|
1345
|
-
flex: 1 1 auto;
|
|
1346
|
-
overflow: hidden;
|
|
1347
|
-
text-overflow: ellipsis;
|
|
1348
|
-
white-space: nowrap;
|
|
1349
|
-
}
|
|
1350
|
-
.cd-transfer__remove {
|
|
1351
|
-
display: inline-flex;
|
|
1352
|
-
align-items: center;
|
|
1353
|
-
justify-content: center;
|
|
1354
|
-
flex: none;
|
|
1355
|
-
inline-size: 18px;
|
|
1356
|
-
block-size: 18px;
|
|
1357
|
-
padding: 0;
|
|
1358
|
-
border: none;
|
|
1359
|
-
border-radius: var(--cd-radius-1, 4px);
|
|
1360
|
-
background: transparent;
|
|
1361
|
-
color: var(--cd-color-text-2);
|
|
1362
|
-
cursor: pointer;
|
|
1363
|
-
}
|
|
1364
|
-
.cd-transfer__remove:hover:not(:disabled) {
|
|
1365
|
-
background: var(--cd-transfer-item-bg-hover);
|
|
1366
|
-
color: var(--cd-color-text-0);
|
|
1367
|
-
}
|
|
1368
|
-
.cd-transfer__remove:disabled {
|
|
1369
|
-
cursor: not-allowed;
|
|
1370
|
-
opacity: 0.5;
|
|
1371
|
-
}
|
|
1372
|
-
.cd-transfer__remove:focus-visible {
|
|
1373
|
-
outline: none;
|
|
1374
|
-
box-shadow: var(--cd-focus-ring);
|
|
1375
|
-
}
|
|
1376
|
-
.cd-transfer__tree-node {
|
|
1377
|
-
display: flex;
|
|
1378
|
-
align-items: center;
|
|
1379
|
-
min-block-size: var(--cd-transfer-item-height);
|
|
1380
|
-
padding-inline-end: var(--cd-spacing-3);
|
|
1381
|
-
}
|
|
1382
|
-
.cd-transfer__tree-node:hover {
|
|
1383
|
-
background: var(--cd-transfer-item-bg-hover);
|
|
1384
|
-
}
|
|
1385
|
-
.cd-transfer__switcher {
|
|
1386
|
-
display: inline-flex;
|
|
1387
|
-
align-items: center;
|
|
1388
|
-
justify-content: center;
|
|
1389
|
-
flex: none;
|
|
1390
|
-
inline-size: 18px;
|
|
1391
|
-
block-size: 18px;
|
|
1392
|
-
padding: 0;
|
|
1393
|
-
border: none;
|
|
1394
|
-
background: transparent;
|
|
1395
|
-
color: var(--cd-color-text-2);
|
|
1396
|
-
cursor: pointer;
|
|
1397
|
-
transition: transform 0.15s ease;
|
|
1398
|
-
}
|
|
1399
|
-
.cd-transfer__switcher--open {
|
|
1400
|
-
transform: rotate(90deg);
|
|
1401
|
-
}
|
|
1402
|
-
.cd-transfer__switcher--leaf {
|
|
1403
|
-
cursor: default;
|
|
1404
|
-
}
|
|
1405
|
-
.cd-transfer__switcher:focus-visible {
|
|
1406
|
-
outline: none;
|
|
1407
|
-
box-shadow: var(--cd-focus-ring);
|
|
1408
|
-
}
|
|
1409
|
-
.cd-transfer__group {
|
|
1410
|
-
display: block;
|
|
1411
|
-
}
|
|
1412
|
-
.cd-transfer__group-header {
|
|
1413
|
-
display: flex;
|
|
1414
|
-
align-items: center;
|
|
1415
|
-
justify-content: space-between;
|
|
1416
|
-
min-block-size: var(--cd-transfer-item-height);
|
|
1417
|
-
padding-inline: var(--cd-spacing-3);
|
|
1418
|
-
background: var(--cd-transfer-panel-header-bg);
|
|
1419
|
-
font-weight: var(--cd-font-weight-medium);
|
|
1420
|
-
}
|
|
1421
|
-
.cd-transfer__group-count {
|
|
1422
|
-
color: var(--cd-color-text-2);
|
|
1423
|
-
font-size: var(--cd-font-size-1);
|
|
1424
|
-
font-weight: var(--cd-font-weight-regular);
|
|
1425
|
-
}
|
|
1426
|
-
.cd-transfer__group-list {
|
|
1427
|
-
margin: 0;
|
|
1428
|
-
padding: 0;
|
|
1429
|
-
list-style: none;
|
|
1430
|
-
}
|
|
1431
|
-
.cd-transfer__empty {
|
|
1432
|
-
display: flex;
|
|
1433
|
-
align-items: center;
|
|
1434
|
-
justify-content: center;
|
|
1435
|
-
padding-block: var(--cd-spacing-4);
|
|
1436
|
-
color: var(--cd-color-text-3);
|
|
1437
|
-
font-size: var(--cd-font-size-1);
|
|
1438
|
-
}
|
|
1439
|
-
.cd-transfer__ops {
|
|
1440
|
-
display: flex;
|
|
1441
|
-
flex-direction: column;
|
|
1442
|
-
justify-content: center;
|
|
1443
|
-
gap: var(--cd-spacing-2);
|
|
1444
|
-
}
|
|
1445
|
-
.cd-transfer__pagination {
|
|
1446
|
-
display: flex;
|
|
1447
|
-
justify-content: center;
|
|
1448
|
-
padding-block: var(--cd-spacing-2);
|
|
1449
|
-
border-block-start: 1px solid var(--cd-transfer-panel-border);
|
|
1450
|
-
}
|
|
1451
|
-
</style>
|