@flux-ui/components 3.0.0-next.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.
Files changed (261) hide show
  1. package/README.md +45 -0
  2. package/package.json +77 -0
  3. package/src/component/FluxAction.vue +27 -0
  4. package/src/component/FluxActionBar.vue +94 -0
  5. package/src/component/FluxActionPane.vue +40 -0
  6. package/src/component/FluxActions.vue +27 -0
  7. package/src/component/FluxAlert.vue +37 -0
  8. package/src/component/FluxAnimatedColors.vue +141 -0
  9. package/src/component/FluxAspectRatio.vue +21 -0
  10. package/src/component/FluxAutoGrid.vue +27 -0
  11. package/src/component/FluxAvatar.vue +119 -0
  12. package/src/component/FluxBadge.vue +84 -0
  13. package/src/component/FluxBadgeStack.vue +18 -0
  14. package/src/component/FluxBorderShine.vue +36 -0
  15. package/src/component/FluxBoxedIcon.vue +36 -0
  16. package/src/component/FluxButton.vue +110 -0
  17. package/src/component/FluxButtonGroup.vue +15 -0
  18. package/src/component/FluxButtonStack.vue +28 -0
  19. package/src/component/FluxCalendar.vue +254 -0
  20. package/src/component/FluxCalendarEvent.vue +41 -0
  21. package/src/component/FluxCheckbox.vue +60 -0
  22. package/src/component/FluxChip.vue +57 -0
  23. package/src/component/FluxClickablePane.vue +61 -0
  24. package/src/component/FluxColorPicker.vue +265 -0
  25. package/src/component/FluxColorSelect.vue +81 -0
  26. package/src/component/FluxComment.vue +71 -0
  27. package/src/component/FluxConfirm.vue +42 -0
  28. package/src/component/FluxContainer.vue +23 -0
  29. package/src/component/FluxDataTable.vue +96 -0
  30. package/src/component/FluxDatePicker.vue +353 -0
  31. package/src/component/FluxDestructiveButton.vue +28 -0
  32. package/src/component/FluxDisabled.vue +22 -0
  33. package/src/component/FluxDivider.vue +37 -0
  34. package/src/component/FluxDotPattern.vue +72 -0
  35. package/src/component/FluxDropZone.vue +202 -0
  36. package/src/component/FluxDynamicView.vue +16 -0
  37. package/src/component/FluxExpandable.vue +119 -0
  38. package/src/component/FluxExpandableGroup.vue +53 -0
  39. package/src/component/FluxFader.vue +64 -0
  40. package/src/component/FluxFaderItem.vue +15 -0
  41. package/src/component/FluxFilter.vue +133 -0
  42. package/src/component/FluxFilterDate.vue +58 -0
  43. package/src/component/FluxFilterDateRange.vue +59 -0
  44. package/src/component/FluxFilterOption.vue +49 -0
  45. package/src/component/FluxFilterOptionAsync.vue +103 -0
  46. package/src/component/FluxFilterOptions.vue +62 -0
  47. package/src/component/FluxFilterOptionsAsync.vue +113 -0
  48. package/src/component/FluxFilterRange.vue +91 -0
  49. package/src/component/FluxFlickeringGrid.vue +141 -0
  50. package/src/component/FluxFlyout.vue +205 -0
  51. package/src/component/FluxFocalPointEditor.vue +137 -0
  52. package/src/component/FluxFocalPointImage.vue +29 -0
  53. package/src/component/FluxForm.vue +35 -0
  54. package/src/component/FluxFormColumn.vue +15 -0
  55. package/src/component/FluxFormDateInput.vue +92 -0
  56. package/src/component/FluxFormDateRangeInput.vue +87 -0
  57. package/src/component/FluxFormDateTimeInput.vue +120 -0
  58. package/src/component/FluxFormField.vue +98 -0
  59. package/src/component/FluxFormFieldAddition.vue +37 -0
  60. package/src/component/FluxFormInput.vue +223 -0
  61. package/src/component/FluxFormInputAddition.vue +31 -0
  62. package/src/component/FluxFormInputGroup.vue +25 -0
  63. package/src/component/FluxFormPinInput.vue +135 -0
  64. package/src/component/FluxFormRangeSlider.vue +179 -0
  65. package/src/component/FluxFormRow.vue +15 -0
  66. package/src/component/FluxFormSection.vue +23 -0
  67. package/src/component/FluxFormSelect.vue +59 -0
  68. package/src/component/FluxFormSelectAsync.vue +118 -0
  69. package/src/component/FluxFormSlider.vue +123 -0
  70. package/src/component/FluxFormTextArea.vue +53 -0
  71. package/src/component/FluxFormTimeZonePicker.vue +713 -0
  72. package/src/component/FluxGallery.vue +99 -0
  73. package/src/component/FluxGalleryItem.vue +49 -0
  74. package/src/component/FluxGrid.vue +28 -0
  75. package/src/component/FluxGridColumn.vue +31 -0
  76. package/src/component/FluxGridPattern.vue +60 -0
  77. package/src/component/FluxIcon.vue +79 -0
  78. package/src/component/FluxInfo.vue +28 -0
  79. package/src/component/FluxInfoStack.vue +17 -0
  80. package/src/component/FluxLegend.vue +27 -0
  81. package/src/component/FluxLink.vue +35 -0
  82. package/src/component/FluxMenu.vue +31 -0
  83. package/src/component/FluxMenuGroup.vue +21 -0
  84. package/src/component/FluxMenuItem.vue +84 -0
  85. package/src/component/FluxMenuOptions.vue +38 -0
  86. package/src/component/FluxMenuSubHeader.vue +33 -0
  87. package/src/component/FluxMenuTitle.vue +17 -0
  88. package/src/component/FluxNotice.vue +79 -0
  89. package/src/component/FluxNoticeStack.vue +17 -0
  90. package/src/component/FluxOverlay.vue +31 -0
  91. package/src/component/FluxPagination.vue +148 -0
  92. package/src/component/FluxPaginationBar.vue +81 -0
  93. package/src/component/FluxPane.vue +45 -0
  94. package/src/component/FluxPaneBody.vue +15 -0
  95. package/src/component/FluxPaneDeck.vue +24 -0
  96. package/src/component/FluxPaneFooter.vue +15 -0
  97. package/src/component/FluxPaneGroup.vue +15 -0
  98. package/src/component/FluxPaneHeader.vue +44 -0
  99. package/src/component/FluxPaneIllustration.vue +68 -0
  100. package/src/component/FluxPaneMedia.vue +31 -0
  101. package/src/component/FluxPercentageBar.vue +45 -0
  102. package/src/component/FluxPersona.vue +48 -0
  103. package/src/component/FluxPlaceholder.vue +56 -0
  104. package/src/component/FluxPressable.vue +77 -0
  105. package/src/component/FluxPrimaryButton.vue +28 -0
  106. package/src/component/FluxProgressBar.vue +75 -0
  107. package/src/component/FluxPrompt.vue +77 -0
  108. package/src/component/FluxPublishButton.vue +59 -0
  109. package/src/component/FluxQuantitySelector.vue +109 -0
  110. package/src/component/FluxRemove.vue +34 -0
  111. package/src/component/FluxRoot.vue +60 -0
  112. package/src/component/FluxSecondaryButton.vue +28 -0
  113. package/src/component/FluxSegmentedControl.vue +77 -0
  114. package/src/component/FluxSegmentedView.vue +15 -0
  115. package/src/component/FluxSeparator.vue +19 -0
  116. package/src/component/FluxSlideOver.vue +25 -0
  117. package/src/component/FluxSnackbar.vue +154 -0
  118. package/src/component/FluxSnackbarProvider.vue +34 -0
  119. package/src/component/FluxSpacer.vue +9 -0
  120. package/src/component/FluxSpacing.vue +32 -0
  121. package/src/component/FluxSpinner.vue +48 -0
  122. package/src/component/FluxSplitButton.vue +61 -0
  123. package/src/component/FluxStack.vue +40 -0
  124. package/src/component/FluxStatistic.vue +68 -0
  125. package/src/component/FluxStepper.vue +69 -0
  126. package/src/component/FluxStepperStep.vue +15 -0
  127. package/src/component/FluxStepperSteps.vue +62 -0
  128. package/src/component/FluxTab.vue +23 -0
  129. package/src/component/FluxTabBar.vue +87 -0
  130. package/src/component/FluxTabBarItem.vue +104 -0
  131. package/src/component/FluxTable.vue +68 -0
  132. package/src/component/FluxTableActions.vue +16 -0
  133. package/src/component/FluxTableCell.vue +47 -0
  134. package/src/component/FluxTableHeader.vue +111 -0
  135. package/src/component/FluxTableRow.vue +15 -0
  136. package/src/component/FluxTabs.vue +91 -0
  137. package/src/component/FluxTag.vue +85 -0
  138. package/src/component/FluxTagStack.vue +18 -0
  139. package/src/component/FluxTicks.vue +44 -0
  140. package/src/component/FluxTimeline.vue +17 -0
  141. package/src/component/FluxTimelineItem.vue +73 -0
  142. package/src/component/FluxToggle.vue +64 -0
  143. package/src/component/FluxToolbar.vue +32 -0
  144. package/src/component/FluxToolbarGroup.vue +18 -0
  145. package/src/component/FluxTooltip.vue +56 -0
  146. package/src/component/FluxTooltipProvider.vue +176 -0
  147. package/src/component/FluxWindow.vue +47 -0
  148. package/src/component/index.ts +142 -0
  149. package/src/component/primitive/Anchor.vue +17 -0
  150. package/src/component/primitive/AnchorPopup.vue +194 -0
  151. package/src/component/primitive/CoordinatePicker.vue +155 -0
  152. package/src/component/primitive/CoordinatePickerThumb.vue +71 -0
  153. package/src/component/primitive/FilterItem.vue +44 -0
  154. package/src/component/primitive/FilterMenuRenderer.ts +233 -0
  155. package/src/component/primitive/FilterOptionBase.vue +67 -0
  156. package/src/component/primitive/SelectBase.vue +340 -0
  157. package/src/component/primitive/SliderBase.vue +89 -0
  158. package/src/component/primitive/SliderThumb.vue +64 -0
  159. package/src/component/primitive/SliderTrack.vue +22 -0
  160. package/src/component/primitive/VNodeRenderer.ts +11 -0
  161. package/src/component/primitive/index.ts +10 -0
  162. package/src/composable/index.ts +9 -0
  163. package/src/composable/private/index.ts +3 -0
  164. package/src/composable/private/useFormSelect.ts +66 -0
  165. package/src/composable/private/useLoaded.ts +21 -0
  166. package/src/composable/private/useTranslate.ts +35 -0
  167. package/src/composable/useBreakpoints.ts +54 -0
  168. package/src/composable/useDisabled.ts +9 -0
  169. package/src/composable/useDisabledInjection.ts +6 -0
  170. package/src/composable/useExpandableGroupInjection.ts +10 -0
  171. package/src/composable/useFilterInjection.ts +22 -0
  172. package/src/composable/useFlyoutInjection.ts +10 -0
  173. package/src/composable/useFormFieldInjection.ts +8 -0
  174. package/src/composable/useTableInjection.ts +11 -0
  175. package/src/css/base.scss +33 -0
  176. package/src/css/component/Action.module.scss +107 -0
  177. package/src/css/component/Avatar.module.scss +177 -0
  178. package/src/css/component/Badge.module.scss +189 -0
  179. package/src/css/component/Button.module.scss +293 -0
  180. package/src/css/component/Calendar.module.scss +171 -0
  181. package/src/css/component/Chip.module.scss +58 -0
  182. package/src/css/component/Color.module.scss +184 -0
  183. package/src/css/component/Comment.module.scss +123 -0
  184. package/src/css/component/DatePicker.module.scss +193 -0
  185. package/src/css/component/Divider.module.scss +79 -0
  186. package/src/css/component/DropZone.module.scss +99 -0
  187. package/src/css/component/Expandable.module.scss +112 -0
  188. package/src/css/component/Fader.module.scss +38 -0
  189. package/src/css/component/Filter.module.scss +80 -0
  190. package/src/css/component/Flyout.module.scss +63 -0
  191. package/src/css/component/FocalPoint.module.scss +84 -0
  192. package/src/css/component/Form.module.scss +812 -0
  193. package/src/css/component/Gallery.module.scss +64 -0
  194. package/src/css/component/Grid.module.scss +24 -0
  195. package/src/css/component/Icon.module.scss +104 -0
  196. package/src/css/component/Info.module.scss +15 -0
  197. package/src/css/component/Layout.module.scss +63 -0
  198. package/src/css/component/Legend.module.scss +32 -0
  199. package/src/css/component/Menu.module.scss +314 -0
  200. package/src/css/component/Notice.module.scss +279 -0
  201. package/src/css/component/Overlay.module.scss +149 -0
  202. package/src/css/component/Pagination.module.scss +59 -0
  203. package/src/css/component/Pane.module.scss +218 -0
  204. package/src/css/component/PercentageBar.module.scss +31 -0
  205. package/src/css/component/Placeholder.module.scss +72 -0
  206. package/src/css/component/Progress.module.scss +84 -0
  207. package/src/css/component/Remove.module.scss +29 -0
  208. package/src/css/component/Root.module.scss +8 -0
  209. package/src/css/component/SegmentedControl.module.scss +82 -0
  210. package/src/css/component/Snackbar.module.scss +227 -0
  211. package/src/css/component/Spinner.module.scss +36 -0
  212. package/src/css/component/Statistic.module.scss +118 -0
  213. package/src/css/component/Stepper.module.scss +103 -0
  214. package/src/css/component/Tab.module.scss +162 -0
  215. package/src/css/component/Table.module.scss +164 -0
  216. package/src/css/component/Timeline.module.scss +173 -0
  217. package/src/css/component/Toolbar.module.scss +82 -0
  218. package/src/css/component/Tooltip.module.scss +62 -0
  219. package/src/css/component/Transition.module.scss +142 -0
  220. package/src/css/component/Visual.module.scss +70 -0
  221. package/src/css/component/base/Button.module.scss +87 -0
  222. package/src/css/component/base/Effect.module.scss +139 -0
  223. package/src/css/component/base/Grid.module.scss +8 -0
  224. package/src/css/component/base/Pane.module.scss +54 -0
  225. package/src/css/component/primitive/CoordinatePicker.module.scss +24 -0
  226. package/src/css/component/primitive/Slider.module.scss +116 -0
  227. package/src/css/index.scss +5 -0
  228. package/src/css/layers.scss +1 -0
  229. package/src/css/mixin/breakpoints.scss +112 -0
  230. package/src/css/mixin/focus-ring.scss +56 -0
  231. package/src/css/mixin/hover.scss +7 -0
  232. package/src/css/mixin/index.scss +3 -0
  233. package/src/css/reset.scss +169 -0
  234. package/src/css/typography.scss +87 -0
  235. package/src/css/variables.scss +214 -0
  236. package/src/data/di.ts +42 -0
  237. package/src/data/filter.ts +9 -0
  238. package/src/data/helper.ts +9 -0
  239. package/src/data/i18n.ts +55 -0
  240. package/src/data/iconRegistry.ts +21 -0
  241. package/src/data/index.ts +8 -0
  242. package/src/data/inputMask.ts +34 -0
  243. package/src/data/store.ts +233 -0
  244. package/src/image/avatar-mask.svg +3 -0
  245. package/src/index.ts +25 -0
  246. package/src/transition/FluxAutoHeightTransition.vue +59 -0
  247. package/src/transition/FluxAutoWidthTransition.vue +59 -0
  248. package/src/transition/FluxBreakthroughTransition.vue +23 -0
  249. package/src/transition/FluxFadeTransition.vue +24 -0
  250. package/src/transition/FluxOverlayTransition.vue +22 -0
  251. package/src/transition/FluxRouteTransition.vue +23 -0
  252. package/src/transition/FluxSlideOverTransition.vue +22 -0
  253. package/src/transition/FluxTooltipTransition.vue +22 -0
  254. package/src/transition/FluxVerticalWindowTransition.vue +23 -0
  255. package/src/transition/FluxWindowTransition.vue +23 -0
  256. package/src/transition/index.ts +10 -0
  257. package/src/util/createDialogRenderer.ts +64 -0
  258. package/src/util/createLabelForDateRange.ts +61 -0
  259. package/src/util/index.ts +2 -0
  260. package/src/vite.d.ts +13 -0
  261. package/tsconfig.json +45 -0
@@ -0,0 +1,233 @@
1
+ import { formatNumber } from '@basmilius/utils';
2
+ import { flattenVNodeTree, getComponentName, getComponentProps } from '@flux-ui/internals';
3
+ import type { FluxFilterBase, FluxFilterDateEntry, FluxFilterDateRangeEntry, FluxFilterItem, FluxFilterOptionEntry, FluxFilterOptionItem, FluxFilterOptionRow, FluxFilterOptionsEntry, FluxFilterRangeEntry, FluxFilterValue, FluxFilterValueSingle } from '@flux-ui/types';
4
+ import { camelCase } from 'lodash-es';
5
+ import { DateTime } from 'luxon';
6
+ import { computed, defineComponent, h, isVNode, unref, VNode } from 'vue';
7
+ import { useTranslate } from '$flux/composable/private';
8
+ import { FluxTranslate, isFluxFilterOptionItem } from '$flux/data';
9
+ import { createLabelForDateRange } from '$flux/util';
10
+ import FluxMenu from '$flux/component/FluxMenu.vue';
11
+ import FluxMenuGroup from '$flux/component/FluxMenuGroup.vue';
12
+ import FluxSeparator from '$flux/component/FluxSeparator.vue';
13
+ import FilterItem from './FilterItem.vue';
14
+
15
+ export const FilterMenuRenderer = defineComponent({
16
+ props: {
17
+ navigate: Function,
18
+ state: Object
19
+ },
20
+
21
+ setup(props, {slots}) {
22
+ const content = computed<(FluxFilterItem | VNode)[][]>(() => {
23
+ const children = flattenVNodeTree(slots.default?.() ?? []);
24
+ const content: (FluxFilterItem | VNode)[][] = [[]];
25
+
26
+ for (const child of children) {
27
+ const name = getComponentName(child);
28
+
29
+ if (name === 'FluxSeparator') {
30
+ content.push([]);
31
+ continue;
32
+ }
33
+
34
+ if (name.startsWith('FluxFilter')) {
35
+ const props = getComponentProps<Omit<FluxFilterItem, 'type'>>(child);
36
+ const type = camelCase(name.substring(10)) as FluxFilterItem['type'];
37
+
38
+ content[content.length - 1].push(parsers[type](props));
39
+ continue;
40
+ }
41
+
42
+ content[content.length - 1].push(child);
43
+ }
44
+
45
+ return content;
46
+ });
47
+
48
+ return () => h(FluxMenu, {}, {
49
+ default: () => unref(content).map((group, index) => renderFilterGroup(group, index, props.navigate!, props.state!))
50
+ });
51
+ }
52
+ });
53
+
54
+ function parseDate(base: FluxFilterBase): FluxFilterDateEntry {
55
+ return {
56
+ ...base,
57
+ type: 'date',
58
+
59
+ async getValueLabel(value): Promise<string | null> {
60
+ if (!DateTime.isDateTime(value)) {
61
+ return null;
62
+ }
63
+
64
+ return value.toLocaleString({
65
+ day: 'numeric',
66
+ month: 'short',
67
+ year: 'numeric'
68
+ });
69
+ }
70
+ };
71
+ }
72
+
73
+ function parseDateRange(base: FluxFilterBase): FluxFilterDateRangeEntry {
74
+ return {
75
+ ...base,
76
+ type: 'dateRange',
77
+
78
+ async getValueLabel(value): Promise<string | null> {
79
+ if (!Array.isArray(value) || value.length !== 2) {
80
+ return null;
81
+ }
82
+
83
+ const [start, end] = value;
84
+
85
+ if (!DateTime.isDateTime(start) || !DateTime.isDateTime(end)) {
86
+ return null;
87
+ }
88
+
89
+ return createLabelForDateRange(start, end);
90
+ }
91
+ };
92
+ }
93
+
94
+ function parseOption(base: FluxFilterBase): FluxFilterOptionEntry {
95
+ const options = (base as any).options as FluxFilterOptionItem[];
96
+
97
+ return {
98
+ ...base,
99
+ type: 'option',
100
+
101
+ async getValueLabel(value): Promise<string | null> {
102
+ return options.find(o => o.value === value)?.label ?? null;
103
+ }
104
+ };
105
+ }
106
+
107
+ function parseOptionAsync(base: FluxFilterBase): FluxFilterOptionEntry {
108
+ const fetchOptions = (base as any).fetchOptions as (ids: FluxFilterValue[]) => Promise<FluxFilterOptionRow[]>;
109
+
110
+ return {
111
+ ...base,
112
+ type: 'option',
113
+
114
+ async getValueLabel(value): Promise<string | null> {
115
+ const options = (await fetchOptions([value])).filter(isFluxFilterOptionItem);
116
+
117
+ return options.find(o => o.value === value)?.label ?? null;
118
+ }
119
+ };
120
+ }
121
+
122
+ function parseOptions(base: FluxFilterBase): FluxFilterOptionsEntry {
123
+ const options = (base as any).options as FluxFilterOptionItem[];
124
+ const translate = useTranslate();
125
+
126
+ return {
127
+ ...base,
128
+ type: 'options',
129
+
130
+ async getValueLabel(value): Promise<string | null> {
131
+ if (!Array.isArray(value)) {
132
+ return null;
133
+ }
134
+
135
+ return generateMultiOptionsLabel(translate, options, value);
136
+ }
137
+ };
138
+ }
139
+
140
+ function parseOptionsAsync(base: FluxFilterBase): FluxFilterOptionsEntry {
141
+ const fetchOptions = (base as any).fetchOptions as (ids: FluxFilterValue[]) => Promise<FluxFilterOptionRow[]>;
142
+ const translate = useTranslate();
143
+
144
+ return {
145
+ ...base,
146
+ type: 'options',
147
+
148
+ async getValueLabel(value): Promise<string | null> {
149
+ if (!Array.isArray(value)) {
150
+ return null;
151
+ }
152
+
153
+ const options = (await fetchOptions(value)).filter(isFluxFilterOptionItem);
154
+
155
+ return generateMultiOptionsLabel(translate, options, value);
156
+ }
157
+ };
158
+ }
159
+
160
+ function parseRange(base: FluxFilterBase): FluxFilterRangeEntry {
161
+ return {
162
+ ...base,
163
+ type: 'range',
164
+
165
+ async getValueLabel(value): Promise<string | null> {
166
+ if (!value || !Array.isArray(value) || value.length !== 2) {
167
+ return null;
168
+ }
169
+
170
+ const [lower, upper] = value as number[];
171
+
172
+ if ('formatter' in base) {
173
+ const formatter = base.formatter as (value: number) => string;
174
+
175
+ return `${formatter(lower)} – ${formatter(upper)}`;
176
+ }
177
+
178
+ return `${formatNumber(lower)} – ${formatNumber(upper)}`;
179
+ }
180
+ };
181
+ }
182
+
183
+ function renderFilterGroup(group: (FluxFilterItem | VNode)[], index: number, navigate: Function, state: Record<string, FluxFilterValue>): VNode[] {
184
+ const slot: VNode[] = [];
185
+
186
+ if (index > 0) {
187
+ slot.push(h(FluxSeparator));
188
+ }
189
+
190
+ slot.push(h(FluxMenuGroup, {}, {
191
+ default: () => group.map(item => renderFilterItem(item, navigate, state))
192
+ }));
193
+
194
+ return slot;
195
+ }
196
+
197
+ function renderFilterItem(item: FluxFilterItem | VNode, navigate: Function, state: Record<string, FluxFilterValue>): VNode {
198
+ if (isVNode(item)) {
199
+ return item;
200
+ }
201
+
202
+ return h(FilterItem, {
203
+ item,
204
+ value: state[item.name] ?? null,
205
+ onClick: () => navigate(item.name)
206
+ });
207
+ }
208
+
209
+ function generateMultiOptionsLabel(translate: FluxTranslate, options: FluxFilterOptionItem[], values: FluxFilterValueSingle[]): string | null {
210
+ const selected = options.filter(o => values.includes(o.value)).length;
211
+
212
+ if (selected <= 0) {
213
+ return null;
214
+ }
215
+
216
+ if (selected === 1) {
217
+ return options.find(o => values.includes(o.value))!.label;
218
+ }
219
+
220
+ return translate('flux.nSelected', {
221
+ n: selected
222
+ });
223
+ }
224
+
225
+ const parsers = {
226
+ date: parseDate,
227
+ dateRange: parseDateRange,
228
+ option: parseOption,
229
+ optionAsync: parseOptionAsync,
230
+ options: parseOptions,
231
+ optionsAsync: parseOptionsAsync,
232
+ range: parseRange
233
+ } as const;
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <FluxMenuGroup>
3
+ <div
4
+ v-if="isSearchable"
5
+ :class="$style.filterSearch">
6
+ <FluxFormInput
7
+ v-model="modelSearch"
8
+ auto-complete="off"
9
+ is-secondary
10
+ icon-leading="magnifying-glass"
11
+ :placeholder="searchPlaceholder"
12
+ type="search"/>
13
+ </div>
14
+
15
+ <FluxMenuItem
16
+ v-if="isLoading && options.length === 0"
17
+ disabled
18
+ is-loading/>
19
+
20
+ <template
21
+ v-else
22
+ v-for="option of options">
23
+ <FluxMenuSubHeader
24
+ v-if="isFluxFilterOptionHeader(option)"
25
+ :label="option.title"/>
26
+
27
+ <FluxMenuItem
28
+ v-else-if="isFluxFilterOptionItem(option)"
29
+ is-selectable
30
+ :is-selected="selected.includes(option.value)"
31
+ :label="option.label"
32
+ @click="select(option)"/>
33
+ </template>
34
+ </FluxMenuGroup>
35
+ </template>
36
+
37
+ <script
38
+ lang="ts"
39
+ setup>
40
+ import type { FluxFilterOptionItem, FluxFilterOptionRow, FluxFilterValueSingle } from '@flux-ui/types';
41
+ import { isFluxFilterOptionHeader, isFluxFilterOptionItem } from '$flux/data';
42
+ import FluxMenuItem from '$flux/component/FluxMenuItem.vue';
43
+ import FluxFormInput from '$flux/component/FluxFormInput.vue';
44
+ import FluxMenuGroup from '$flux/component/FluxMenuGroup.vue';
45
+ import FluxMenuSubHeader from '$flux/component/FluxMenuSubHeader.vue';
46
+ import $style from '$flux/css/component/Filter.module.scss';
47
+
48
+ const emit = defineEmits<{
49
+ select: [FluxFilterValueSingle];
50
+ }>();
51
+
52
+ const modelSearch = defineModel<string>('searchQuery', {
53
+ default: ''
54
+ });
55
+
56
+ defineProps<{
57
+ readonly isLoading?: boolean;
58
+ readonly isSearchable?: boolean;
59
+ readonly options: FluxFilterOptionRow[];
60
+ readonly selected: FluxFilterValueSingle[];
61
+ readonly searchPlaceholder?: string;
62
+ }>();
63
+
64
+ function select(option: FluxFilterOptionItem): void {
65
+ emit('select', option.value);
66
+ }
67
+ </script>
@@ -0,0 +1,340 @@
1
+ <template>
2
+ <Anchor
3
+ ref="anchor"
4
+ :="$attrs"
5
+ :class="clsx(
6
+ $style.formSelect,
7
+ disabled && $style.isDisabled,
8
+ isPopupOpen && $style.isFocused,
9
+ isSearchable && $style.isSearchable
10
+ )"
11
+ :id="id"
12
+ :aria-disabled="disabled ? true : undefined"
13
+ tabindex="0"
14
+ tag-name="div"
15
+ @click="toggle()"
16
+ @keydown="onKeyDown"
17
+ @keyup="onKeyUp">
18
+ <template v-if="!isMultiple && selected[0]">
19
+ <FluxMenuItem
20
+ :class="$style.formSelectSelected"
21
+ :command="selected[0].command"
22
+ :command-icon="selected[0].commandIcon"
23
+ :icon-leading="selected[0].icon"
24
+ :label="selected[0].label"
25
+ tabindex="-1"/>
26
+ </template>
27
+
28
+ <template v-else-if="isMultiple && selected[0]">
29
+ <FluxTag
30
+ v-for="option of selected"
31
+ :key="option.value ?? 'null option'"
32
+ :label="option.label"
33
+ is-deletable
34
+ @delete="deselect(option.value)"/>
35
+ </template>
36
+
37
+ <template v-else-if="placeholder">
38
+ <span :class="$style.formSelectPlaceholder">
39
+ {{ placeholder }}
40
+ </span>
41
+ </template>
42
+
43
+ <FluxSpinner
44
+ v-if="isLoading"
45
+ :class="$style.formSelectIcon"
46
+ :size="16"/>
47
+
48
+ <FluxIcon
49
+ v-else
50
+ :class="$style.formSelectIcon"
51
+ name="angle-down"/>
52
+ </Anchor>
53
+
54
+ <Teleport to="body">
55
+ <FluxFadeTransition>
56
+ <AnchorPopup
57
+ v-if="isPopupOpen && !disabled"
58
+ ref="anchorPopup"
59
+ :class="clsx(
60
+ $style.formSelectPopup,
61
+ isKeyboardAction && $style.isKeyboardAction,
62
+ isSearchable && $style.isSearchable
63
+ )"
64
+ :anchor="anchorRef"
65
+ direction="vertical"
66
+ use-anchor-width>
67
+ <FluxFormInput
68
+ v-if="isSearchable"
69
+ v-model="modelSearch"
70
+ ref="searchInputElement"
71
+ auto-complete="off"
72
+ :class="$style.formSelectInput"
73
+ type="search"
74
+ icon-trailing="magnifying-glass"
75
+ :placeholder="translate('flux.search')"
76
+ @keydown="onKeyDown"/>
77
+
78
+ <FluxMenu v-if="!isLoading && options.length === 0">
79
+ <FluxMenuSubHeader :label="translate('flux.noItems')"/>
80
+ </FluxMenu>
81
+
82
+ <FluxMenu v-else>
83
+ <template
84
+ v-for="([item, subItems], index) of options"
85
+ :key="`group-${index}`">
86
+ <FluxMenuGroup>
87
+ <FluxMenuSubHeader
88
+ v-if="isFluxFormSelectGroup(item)"
89
+ :icon-leading="item.icon"
90
+ :label="item.label"/>
91
+
92
+ <template v-for="(subItem, index) of subItems">
93
+ <FluxMenuItem
94
+ v-if="isFluxFormSelectOption(subItem)"
95
+ ref="optionElements"
96
+ :key="index"
97
+ :command="subItem.command"
98
+ :command-icon="subItem.commandIcon"
99
+ :icon-leading="subItem.icon"
100
+ :is-active="!!selected.find(so => so.value === subItem.value)"
101
+ :is-highlighted="highlightedId === subItem.value"
102
+ :label="subItem.label"
103
+ type="button"
104
+ @click="select(subItem.value)"/>
105
+ </template>
106
+ </FluxMenuGroup>
107
+
108
+ <FluxMenuItem
109
+ v-if="isFluxFormSelectOption(item)"
110
+ ref="optionElements"
111
+ :key="`item-${index}`"
112
+ :command="item.command"
113
+ :command-icon="item.commandIcon"
114
+ :icon-leading="item.icon"
115
+ :is-active="!!selected.find(so => so.value === item.value)"
116
+ :is-highlighted="highlightedId === item.value"
117
+ :label="item.label"
118
+ type="button"
119
+ @click="select(item.value)"/>
120
+ </template>
121
+ </FluxMenu>
122
+ </AnchorPopup>
123
+ </FluxFadeTransition>
124
+ </Teleport>
125
+ </template>
126
+
127
+ <script
128
+ lang="ts"
129
+ setup>
130
+ import { unrefTemplateElement, useClickOutside } from '@flux-ui/internals';
131
+ import type { FluxFormSelectOption, FluxFormSelectOptions } from '@flux-ui/types';
132
+ import { clsx } from 'clsx';
133
+ import { ComponentPublicInstance, computed, nextTick, ref, toRef, unref, useTemplateRef, watch } from 'vue';
134
+ import { useDisabled, useFormFieldInjection } from '$flux/composable';
135
+ import { useTranslate } from '$flux/composable/private';
136
+ import { isFluxFormSelectGroup, isFluxFormSelectOption } from '$flux/data';
137
+ import { FluxFadeTransition } from '$flux/transition';
138
+ import FluxFormInput from '$flux/component/FluxFormInput.vue';
139
+ import FluxIcon from '$flux/component/FluxIcon.vue';
140
+ import FluxMenu from '$flux/component/FluxMenu.vue';
141
+ import FluxMenuGroup from '$flux/component/FluxMenuGroup.vue';
142
+ import FluxMenuItem from '$flux/component/FluxMenuItem.vue';
143
+ import FluxMenuSubHeader from '$flux/component/FluxMenuSubHeader.vue';
144
+ import FluxSpinner from '$flux/component/FluxSpinner.vue';
145
+ import FluxTag from '$flux/component/FluxTag.vue';
146
+ import Anchor from './Anchor.vue';
147
+ import AnchorPopup from './AnchorPopup.vue';
148
+ import $style from '$flux/css/component/Form.module.scss';
149
+
150
+ const INITIAL_HIGHLIGHTED_INDEX = -1;
151
+
152
+ const emit = defineEmits<{
153
+ keyDown: [KeyboardEvent];
154
+ deselect: [string | number | null];
155
+ select: [string | number | null];
156
+ search: [string];
157
+ close: [];
158
+ open: [];
159
+ }>();
160
+
161
+ const modelSearch = defineModel<string>('searchQuery', {
162
+ default: ''
163
+ });
164
+
165
+ defineOptions({
166
+ inheritAttrs: false
167
+ });
168
+
169
+ const {
170
+ disabled: componentDisabled,
171
+ isMultiple,
172
+ options,
173
+ selected
174
+ } = defineProps<{
175
+ readonly disabled?: boolean;
176
+ readonly isLoading?: boolean;
177
+ readonly isMultiple?: boolean;
178
+ readonly isSearchable?: boolean;
179
+ readonly options: FluxFormSelectOptions[];
180
+ readonly placeholder?: string;
181
+ readonly selected: FluxFormSelectOption[];
182
+ }>();
183
+
184
+ const disabled = useDisabled(toRef(() => componentDisabled));
185
+ const {id} = useFormFieldInjection();
186
+ const translate = useTranslate();
187
+
188
+ const anchorRef = useTemplateRef<ComponentPublicInstance>('anchor');
189
+ const anchorPopupRef = useTemplateRef('anchorPopup');
190
+ const optionElementRefs = useTemplateRef<typeof FluxMenuItem[]>('optionElements');
191
+ const searchInputElementRef = useTemplateRef<ComponentPublicInstance<typeof FluxFormInput>>('searchInputElement');
192
+
193
+ const highlightedIndex = ref(INITIAL_HIGHLIGHTED_INDEX);
194
+ const isKeyboardAction = ref(false);
195
+ const isPopupOpen = ref(false);
196
+
197
+ const focusElement = computed(() => unrefTemplateElement(searchInputElementRef) ?? unrefTemplateElement(anchorRef));
198
+ const highlightedId = computed(() => unref(rawOptions)[unref(highlightedIndex)]?.value);
199
+ const rawOptions = computed(() => options.map(group => group[1]).flat());
200
+
201
+ useClickOutside([anchorRef, anchorPopupRef], isPopupOpen, () => isPopupOpen.value = false);
202
+ useClickOutside(anchorRef, isPopupOpen, () => unref(focusElement)?.focus());
203
+
204
+ function deselect(id: string | number | null): void {
205
+ emit('deselect', id);
206
+
207
+ nextTick(() => unref(focusElement)?.focus());
208
+ }
209
+
210
+ function select(id: string | number | null): void {
211
+ emit('select', id);
212
+
213
+ !isMultiple && (isPopupOpen.value = false);
214
+
215
+ highlightedIndex.value = INITIAL_HIGHLIGHTED_INDEX;
216
+ modelSearch.value = '';
217
+
218
+ nextTick(() => unref(focusElement)?.focus());
219
+ }
220
+
221
+ function toggle(): void {
222
+ if (unref(disabled)) {
223
+ return;
224
+ }
225
+
226
+ isPopupOpen.value = !unref(isPopupOpen);
227
+ }
228
+
229
+ function onKeyDown(evt: KeyboardEvent): void {
230
+ emit('keyDown', evt);
231
+
232
+ if (!unref(isPopupOpen)) {
233
+ if (evt.key === 'Enter') {
234
+ isPopupOpen.value = true;
235
+ }
236
+
237
+ return;
238
+ }
239
+
240
+ isKeyboardAction.value = true;
241
+
242
+ if (unref(highlightedIndex) === INITIAL_HIGHLIGHTED_INDEX && ['ArrowDown', 'ArrowUp'].includes(evt.key)) {
243
+ const options = unref(optionElementRefs);
244
+ const selectedIndex = options?.findIndex(o => 'isActive' in o.$props && o.$props.isActive);
245
+
246
+ highlightedIndex.value = selectedIndex ?? INITIAL_HIGHLIGHTED_INDEX;
247
+ }
248
+
249
+ switch (evt.key) {
250
+ case 'ArrowUp':
251
+ highlightedIndex.value = Math.max(0, unref(highlightedIndex) - 1);
252
+ break;
253
+
254
+ case 'ArrowDown':
255
+ highlightedIndex.value = Math.min(unref(rawOptions).length - 1, unref(highlightedIndex) + 1);
256
+ break;
257
+
258
+ case 'Backspace':
259
+ const search = unref(modelSearch);
260
+
261
+ if (search.length > 0 || selected.length === 0) {
262
+ return;
263
+ }
264
+
265
+ deselect(selected[selected.length - 1].value);
266
+ break;
267
+
268
+ case 'Enter':
269
+ const id = unref(highlightedId);
270
+ id && select(id);
271
+ break;
272
+
273
+ case 'Escape':
274
+ isPopupOpen.value = false;
275
+ break;
276
+
277
+ case 'Tab':
278
+ isPopupOpen.value = false;
279
+ return;
280
+
281
+ default:
282
+ if (evt.key.match(/[a-z]/)) {
283
+ highlightedIndex.value = unref(rawOptions).findIndex(o => o.label.toLowerCase().startsWith(evt.key));
284
+ } else {
285
+ highlightedIndex.value = -1;
286
+ }
287
+ return;
288
+ }
289
+
290
+ evt.preventDefault();
291
+ }
292
+
293
+ function onKeyUp(): void {
294
+ isKeyboardAction.value = false;
295
+ }
296
+
297
+ watch(highlightedIndex, highlightedIndex => {
298
+ const options = unref(optionElementRefs)!;
299
+ options[highlightedIndex]?.$el.scrollIntoView({
300
+ block: 'center'
301
+ });
302
+ });
303
+
304
+ watch(isPopupOpen, isPopupOpen => {
305
+ if (!isPopupOpen) {
306
+ emit('close');
307
+ return;
308
+ }
309
+
310
+ nextTick(() => {
311
+ const searchInput = unref(searchInputElementRef);
312
+ searchInput?.focus();
313
+ });
314
+
315
+ nextTick(() => {
316
+ const options = unref(optionElementRefs);
317
+
318
+ if (!options || isMultiple) {
319
+ return;
320
+ }
321
+
322
+ const selectedIndex = options.findIndex(o => 'isActive' in o.$props && o.$props.isActive);
323
+ const option = options[selectedIndex];
324
+
325
+ if (!option) {
326
+ return;
327
+ }
328
+
329
+ option.$el.scrollIntoView({
330
+ block: 'center'
331
+ });
332
+ });
333
+
334
+ emit('open');
335
+ });
336
+
337
+ watch(modelSearch, searchQuery => emit('search', searchQuery));
338
+
339
+ watch([() => options, isPopupOpen], () => highlightedIndex.value = INITIAL_HIGHLIGHTED_INDEX);
340
+ </script>