@fastkit/vui 0.8.8 → 0.8.11
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/vui.cjs.js +176 -50
- package/dist/vui.cjs.prod.js +176 -50
- package/dist/vui.css +46 -4
- package/dist/vui.d.ts +42 -43
- package/dist/vui.min.css +1 -1
- package/dist/vui.min.css.map +1 -1
- package/dist/vui.mjs +176 -53
- package/package.json +6 -6
- package/src/components/VOption/VOption.scss +17 -4
- package/src/components/VOption/VOption.tsx +8 -1
- package/src/components/VOptionGroup/VOptionGroup.scss +13 -0
- package/src/components/VSelect/VSelect.scss +14 -0
- package/src/components/VSelect/VSelect.tsx +171 -9
- package/src/composables/form-selector.tsx +26 -17
- package/src/plugin.tsx +3 -11
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import './VSelect.scss';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
Ref,
|
|
5
|
+
VNodeChild,
|
|
6
|
+
defineComponent,
|
|
7
|
+
PropType,
|
|
8
|
+
computed,
|
|
9
|
+
nextTick,
|
|
10
|
+
} from 'vue';
|
|
3
11
|
import {
|
|
4
12
|
createFormSelectorSettings,
|
|
5
13
|
useFormSelectorControl,
|
|
@@ -10,6 +18,7 @@ import {
|
|
|
10
18
|
createPropsOptions,
|
|
11
19
|
VNodeChildOrSlot,
|
|
12
20
|
resolveVNodeChildOrSlots,
|
|
21
|
+
useKeybord,
|
|
13
22
|
} from '@fastkit/vue-kit';
|
|
14
23
|
import { VFormControl } from '../VFormControl';
|
|
15
24
|
import {
|
|
@@ -26,8 +35,19 @@ import {
|
|
|
26
35
|
import { VUI_SELECT_SYMBOL, useVui } from '../../injections';
|
|
27
36
|
import { VIcon } from '../VIcon';
|
|
28
37
|
import { VMenu } from '../kits';
|
|
38
|
+
import { VOptionGroup } from '../VOptionGroup';
|
|
29
39
|
import { VOption } from '../VOption';
|
|
30
40
|
import { VButton } from '..';
|
|
41
|
+
import { VMenuControl } from '@fastkit/vue-stack';
|
|
42
|
+
|
|
43
|
+
export const ARROW_KEY_TYPES = useKeybord.Key(['ArrowUp', 'ArrowDown']);
|
|
44
|
+
|
|
45
|
+
export const CHOICE_KEY_TYPES = useKeybord.Key(['Enter', ' ']);
|
|
46
|
+
|
|
47
|
+
export const KEYBORD_EVENT_TYPES = useKeybord.Key([
|
|
48
|
+
...ARROW_KEY_TYPES,
|
|
49
|
+
...CHOICE_KEY_TYPES,
|
|
50
|
+
]);
|
|
31
51
|
|
|
32
52
|
const { props, emits } = createFormSelectorSettings();
|
|
33
53
|
|
|
@@ -48,6 +68,7 @@ export const VSelect = defineComponent({
|
|
|
48
68
|
},
|
|
49
69
|
emits,
|
|
50
70
|
setup(props, ctx) {
|
|
71
|
+
const menuRef: Ref<{ stackMenuControl: VMenuControl } | null> = ref(null);
|
|
51
72
|
const menuOpened = ref(false);
|
|
52
73
|
const showMenu = () => {
|
|
53
74
|
menuOpened.value = true;
|
|
@@ -111,9 +132,128 @@ export const VSelect = defineComponent({
|
|
|
111
132
|
<span class="v-select__placeholder">{props.placeholder}</span>,
|
|
112
133
|
);
|
|
113
134
|
}
|
|
135
|
+
|
|
114
136
|
return children;
|
|
115
137
|
};
|
|
116
138
|
|
|
139
|
+
const clearKeyFocused = () => {
|
|
140
|
+
const menu = menuRef.value;
|
|
141
|
+
if (!menu) return;
|
|
142
|
+
const bodyEl = menu.stackMenuControl.bodyRef.value;
|
|
143
|
+
if (!bodyEl) return;
|
|
144
|
+
|
|
145
|
+
const els = Array.from(
|
|
146
|
+
bodyEl.querySelectorAll('.v-option'),
|
|
147
|
+
) as HTMLElement[];
|
|
148
|
+
|
|
149
|
+
els.forEach((el) => el.classList.remove('v-option--key-focused'));
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const getItemElements = (): HTMLElement[] | void => {
|
|
153
|
+
const menu = menuRef.value;
|
|
154
|
+
if (!menu) return;
|
|
155
|
+
|
|
156
|
+
const bodyEl = menu.stackMenuControl.bodyRef.value;
|
|
157
|
+
|
|
158
|
+
if (!bodyEl) return;
|
|
159
|
+
|
|
160
|
+
const els = (
|
|
161
|
+
Array.from(bodyEl.querySelectorAll('.v-option')) as HTMLElement[]
|
|
162
|
+
).filter((el) => {
|
|
163
|
+
if (el.tabIndex === -1) return false;
|
|
164
|
+
const disabled = el.getAttribute('disabled');
|
|
165
|
+
const ariaDisabled = el.getAttribute('aria-disabled');
|
|
166
|
+
if (ariaDisabled === 'true') return false;
|
|
167
|
+
return disabled == null || disabled === '';
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!els.length) return;
|
|
171
|
+
|
|
172
|
+
return els;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const arrowKeyHandler = (ev: KeyboardEvent) => {
|
|
176
|
+
const { key } = ev;
|
|
177
|
+
if (!menuOpened.value || !ARROW_KEY_TYPES.includes(key as any)) return;
|
|
178
|
+
|
|
179
|
+
const els = getItemElements();
|
|
180
|
+
|
|
181
|
+
if (!els) return;
|
|
182
|
+
|
|
183
|
+
const currentEl = els.find((el) =>
|
|
184
|
+
el.classList.contains('v-option--key-focused'),
|
|
185
|
+
);
|
|
186
|
+
const currentIndex = currentEl && els.indexOf(currentEl);
|
|
187
|
+
let nextIndex: number;
|
|
188
|
+
const { length } = els;
|
|
189
|
+
const isUp = key === 'ArrowUp';
|
|
190
|
+
if (currentIndex == null) {
|
|
191
|
+
nextIndex = isUp ? length - 1 : 0;
|
|
192
|
+
} else {
|
|
193
|
+
const shiftAmount = key === 'ArrowUp' ? -1 : 1;
|
|
194
|
+
nextIndex = currentIndex + shiftAmount;
|
|
195
|
+
if (nextIndex < 0) {
|
|
196
|
+
nextIndex = length - 1;
|
|
197
|
+
} else if (nextIndex >= length) {
|
|
198
|
+
nextIndex = 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const nextEl = els[nextIndex];
|
|
203
|
+
|
|
204
|
+
if (nextEl) {
|
|
205
|
+
clearKeyFocused();
|
|
206
|
+
nextEl.classList.add('v-option--key-focused');
|
|
207
|
+
nextEl.scrollIntoView({
|
|
208
|
+
block: 'nearest',
|
|
209
|
+
inline: 'nearest',
|
|
210
|
+
behavior: 'smooth',
|
|
211
|
+
});
|
|
212
|
+
ev.preventDefault();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const choiceKeyHandler = (ev: KeyboardEvent) => {
|
|
217
|
+
const { key } = ev;
|
|
218
|
+
if (!menuOpened.value || !CHOICE_KEY_TYPES.includes(key as any)) return;
|
|
219
|
+
|
|
220
|
+
const els = getItemElements();
|
|
221
|
+
|
|
222
|
+
if (!els) return;
|
|
223
|
+
|
|
224
|
+
const currentEl = els.find((el) =>
|
|
225
|
+
el.classList.contains('v-option--key-focused'),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (currentEl) {
|
|
229
|
+
const ev = new MouseEvent('click', {
|
|
230
|
+
view: window,
|
|
231
|
+
bubbles: true,
|
|
232
|
+
cancelable: true,
|
|
233
|
+
});
|
|
234
|
+
currentEl.dispatchEvent(ev);
|
|
235
|
+
ev.preventDefault();
|
|
236
|
+
nextTick(() => {
|
|
237
|
+
currentEl.classList.add('v-option--key-focused');
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const keybordEventHandler = (ev: KeyboardEvent) => {
|
|
243
|
+
if (ARROW_KEY_TYPES.includes(ev.key as any)) return arrowKeyHandler(ev);
|
|
244
|
+
if (CHOICE_KEY_TYPES.includes(ev.key as any)) return choiceKeyHandler(ev);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
useKeybord(
|
|
248
|
+
[
|
|
249
|
+
{
|
|
250
|
+
key: KEYBORD_EVENT_TYPES,
|
|
251
|
+
handler: keybordEventHandler,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
{ autorun: true },
|
|
255
|
+
);
|
|
256
|
+
|
|
117
257
|
return {
|
|
118
258
|
...inputControl.expose(),
|
|
119
259
|
...control,
|
|
@@ -124,18 +264,31 @@ export const VSelect = defineComponent({
|
|
|
124
264
|
showMenu,
|
|
125
265
|
closeMenu,
|
|
126
266
|
renderSelections,
|
|
267
|
+
menuRef: () => menuRef,
|
|
268
|
+
clearKeyFocused,
|
|
127
269
|
};
|
|
128
270
|
},
|
|
129
271
|
render() {
|
|
130
272
|
const { nodeControl, selectorControl, selectorItems, selectedItems } = this;
|
|
131
273
|
const children = (this.$slots.default && this.$slots.default()) || [];
|
|
132
|
-
const
|
|
274
|
+
const propGroups = this.propGroups.map((group) => {
|
|
133
275
|
return (
|
|
134
|
-
<
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
276
|
+
<VOptionGroup
|
|
277
|
+
key={group.id}
|
|
278
|
+
groupId={group.id}
|
|
279
|
+
label={group.label(selectorControl)}
|
|
280
|
+
disabled={group.disabled}>
|
|
281
|
+
{group.items.map((item) => (
|
|
282
|
+
<VOption
|
|
283
|
+
disabled={item.disabled}
|
|
284
|
+
value={item.value}
|
|
285
|
+
key={item.value}>
|
|
286
|
+
{{
|
|
287
|
+
default: () => item.label(selectorControl),
|
|
288
|
+
}}
|
|
289
|
+
</VOption>
|
|
290
|
+
))}
|
|
291
|
+
</VOptionGroup>
|
|
139
292
|
);
|
|
140
293
|
});
|
|
141
294
|
|
|
@@ -143,7 +296,13 @@ export const VSelect = defineComponent({
|
|
|
143
296
|
<VFormControl
|
|
144
297
|
nodeControl={nodeControl}
|
|
145
298
|
// focused={this.nodeControl.focused}
|
|
146
|
-
class={[
|
|
299
|
+
class={[
|
|
300
|
+
'v-select',
|
|
301
|
+
this.classes,
|
|
302
|
+
{
|
|
303
|
+
'v-select--multiple': this.multiple,
|
|
304
|
+
},
|
|
305
|
+
]}
|
|
147
306
|
label={this.label}
|
|
148
307
|
hint={this.hint}
|
|
149
308
|
hinttip={this.hinttip}
|
|
@@ -161,6 +320,8 @@ export const VSelect = defineComponent({
|
|
|
161
320
|
distance={0}
|
|
162
321
|
alwaysRender
|
|
163
322
|
v-model={this.menuOpened}
|
|
323
|
+
ref={this.menuRef()}
|
|
324
|
+
onClose={this.clearKeyFocused}
|
|
164
325
|
v-slots={{
|
|
165
326
|
activator: ({ attrs, control }) => [
|
|
166
327
|
<VControlField
|
|
@@ -173,6 +334,7 @@ export const VSelect = defineComponent({
|
|
|
173
334
|
// tabindex={this.computedTabindex}
|
|
174
335
|
size={this.size}
|
|
175
336
|
focused={this.menuOpened}
|
|
337
|
+
autoHeight={this.multiple}
|
|
176
338
|
onClick={(ev) => {
|
|
177
339
|
if (this.canOperation && !control.isActive) {
|
|
178
340
|
let t = ev.target as HTMLElement;
|
|
@@ -261,7 +423,7 @@ export const VSelect = defineComponent({
|
|
|
261
423
|
}}>
|
|
262
424
|
<div class={['v-select__body', this.classes]}>
|
|
263
425
|
{children}
|
|
264
|
-
{
|
|
426
|
+
{propGroups}
|
|
265
427
|
</div>
|
|
266
428
|
</VMenu>
|
|
267
429
|
),
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
FormControlSlots,
|
|
9
9
|
useFormSelectorControl,
|
|
10
10
|
FormSelectorControl,
|
|
11
|
-
|
|
11
|
+
ResolvedFormSelectorItem,
|
|
12
12
|
FormNodeControl,
|
|
13
13
|
renderSlotOrEmpty,
|
|
14
14
|
VNodeChildOrSlot,
|
|
@@ -28,7 +28,7 @@ export interface DefineFormSelectorComponentOptions {
|
|
|
28
28
|
itemRenderer: (ctx: {
|
|
29
29
|
control: FormSelectorControl;
|
|
30
30
|
selected: boolean;
|
|
31
|
-
attrs:
|
|
31
|
+
attrs: ResolvedFormSelectorItem & {
|
|
32
32
|
modelValue: boolean;
|
|
33
33
|
key: string | number;
|
|
34
34
|
};
|
|
@@ -70,7 +70,7 @@ export function defineFormSelectorComponent(
|
|
|
70
70
|
});
|
|
71
71
|
const control = useControl(props);
|
|
72
72
|
const vui = useVui();
|
|
73
|
-
const loadingMessageRef = computed(() => {
|
|
73
|
+
const loadingMessageRef = computed<VNodeChild | undefined>(() => {
|
|
74
74
|
const slot = resolveVNodeChildOrSlots(
|
|
75
75
|
props.loadingMessage,
|
|
76
76
|
vui.setting('loadingMessage'),
|
|
@@ -135,20 +135,29 @@ export function defineFormSelectorComponent(
|
|
|
135
135
|
{this.loadingMessageRef}
|
|
136
136
|
</div>
|
|
137
137
|
)}
|
|
138
|
-
{this.
|
|
139
|
-
const
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
138
|
+
{this.propGroups.map((group) => {
|
|
139
|
+
const { items } = group;
|
|
140
|
+
return (
|
|
141
|
+
<>
|
|
142
|
+
{items.map((attrs) => {
|
|
143
|
+
const selected = selectorControl.isSelected(
|
|
144
|
+
attrs.value,
|
|
145
|
+
);
|
|
146
|
+
return itemRenderer({
|
|
147
|
+
selected,
|
|
148
|
+
control: selectorControl,
|
|
149
|
+
attrs: {
|
|
150
|
+
...attrs,
|
|
151
|
+
modelValue: selected,
|
|
152
|
+
key: attrs.value,
|
|
153
|
+
},
|
|
154
|
+
slots: {
|
|
155
|
+
default: () => attrs.label(this.selectorControl),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
})}
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
152
161
|
})}
|
|
153
162
|
{renderSlotOrEmpty(this.$slots, 'default')}
|
|
154
163
|
</div>
|
package/src/plugin.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
installVueStackPlugin,
|
|
11
11
|
VueStackServiceOptions,
|
|
12
12
|
VueColorSchemePlugin,
|
|
13
|
+
onAppUnmount,
|
|
13
14
|
} from '@fastkit/vue-kit';
|
|
14
15
|
import { VButton } from './components/VButton';
|
|
15
16
|
|
|
@@ -46,14 +47,7 @@ declare module '@vue/runtime-core' {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export class VuiPlugin {
|
|
49
|
-
static readonly installedApps = new Set<App>();
|
|
50
|
-
|
|
51
50
|
static install(app: App, opts: VuiPluginOptions) {
|
|
52
|
-
const { installedApps } = this;
|
|
53
|
-
if (installedApps.has(app)) return;
|
|
54
|
-
const unmountApp = app.unmount;
|
|
55
|
-
installedApps.add(app);
|
|
56
|
-
|
|
57
51
|
const { colorScheme, stack, uiSettings } = opts;
|
|
58
52
|
|
|
59
53
|
// ColorScheme
|
|
@@ -88,11 +82,9 @@ export class VuiPlugin {
|
|
|
88
82
|
app.provide(VuiInjectionKey, $vui);
|
|
89
83
|
app.config.globalProperties.$vui = $vui;
|
|
90
84
|
|
|
91
|
-
app
|
|
92
|
-
installedApps.delete(app);
|
|
85
|
+
onAppUnmount(app, () => {
|
|
93
86
|
delete app.config.globalProperties.$vui;
|
|
94
|
-
|
|
95
|
-
};
|
|
87
|
+
});
|
|
96
88
|
}
|
|
97
89
|
}
|
|
98
90
|
|