@flux-ui/components 3.1.2 → 3.1.4
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/component/FluxAvatarGroup.vue.d.ts +17 -0
- package/dist/component/FluxButton.vue.d.ts +2 -0
- package/dist/component/FluxContextMenu.vue.d.ts +26 -0
- package/dist/component/FluxDataTable.vue.d.ts +20 -10
- package/dist/component/FluxDescriptionItem.vue.d.ts +19 -0
- package/dist/component/FluxDescriptionList.vue.d.ts +17 -0
- package/dist/component/FluxFlyout.vue.d.ts +9 -2
- package/dist/component/FluxFormCombobox.vue.d.ts +20 -0
- package/dist/component/FluxFormRating.vue.d.ts +21 -0
- package/dist/component/FluxFormTagsInput.vue.d.ts +27 -0
- package/dist/component/FluxFormTextArea.vue.d.ts +6 -1
- package/dist/component/FluxInlineEdit.vue.d.ts +41 -0
- package/dist/component/FluxMenu.vue.d.ts +1 -0
- package/dist/component/FluxMenuFlyout.vue.d.ts +22 -0
- package/dist/component/FluxTableCell.vue.d.ts +1 -0
- package/dist/component/FluxTour.vue.d.ts +35 -0
- package/dist/component/FluxTourItem.vue.d.ts +18 -0
- package/dist/component/FluxVirtualScroller.vue.d.ts +27 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/component/primitive/AnchorPopup.vue.d.ts +7 -1
- package/dist/component/primitive/SelectBase.vue.d.ts +3 -0
- package/dist/composable/private/index.d.ts +1 -0
- package/dist/composable/private/useMenuFlyout.d.ts +42 -0
- package/dist/data/di.d.ts +35 -0
- package/dist/data/i18n.d.ts +7 -0
- package/dist/index.css +449 -5
- package/dist/index.js +2156 -408
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/component/FluxAvatarGroup.vue +52 -0
- package/src/component/FluxButton.vue +3 -0
- package/src/component/FluxContextMenu.vue +134 -0
- package/src/component/FluxDataTable.vue +113 -32
- package/src/component/FluxDescriptionItem.vue +43 -0
- package/src/component/FluxDescriptionList.vue +37 -0
- package/src/component/FluxDestructiveButton.vue +2 -1
- package/src/component/FluxFlyout.vue +16 -3
- package/src/component/FluxFormCombobox.vue +98 -0
- package/src/component/FluxFormRating.vue +172 -0
- package/src/component/FluxFormTagsInput.vue +249 -0
- package/src/component/FluxFormTextArea.vue +16 -1
- package/src/component/FluxInlineEdit.vue +176 -0
- package/src/component/FluxMenu.vue +13 -3
- package/src/component/FluxMenuFlyout.vue +118 -0
- package/src/component/FluxPrimaryButton.vue +2 -1
- package/src/component/FluxPrimaryLinkButton.vue +2 -1
- package/src/component/FluxPublishButton.vue +2 -1
- package/src/component/FluxSecondaryButton.vue +2 -1
- package/src/component/FluxSecondaryLinkButton.vue +2 -1
- package/src/component/FluxTableCell.vue +2 -0
- package/src/component/FluxTour.vue +332 -0
- package/src/component/FluxTourItem.vue +27 -0
- package/src/component/FluxVirtualScroller.vue +96 -0
- package/src/component/index.ts +12 -0
- package/src/component/primitive/AnchorPopup.vue +27 -0
- package/src/component/primitive/SelectBase.vue +37 -2
- package/src/composable/private/index.ts +1 -0
- package/src/composable/private/useMenuFlyout.ts +417 -0
- package/src/css/component/AvatarGroup.module.scss +22 -0
- package/src/css/component/ContextMenu.module.scss +17 -0
- package/src/css/component/DescriptionList.module.scss +98 -0
- package/src/css/component/Form.module.scss +51 -0
- package/src/css/component/FormRating.module.scss +47 -0
- package/src/css/component/InlineEdit.module.scss +45 -0
- package/src/css/component/Menu.module.scss +4 -1
- package/src/css/component/MenuFlyout.module.scss +38 -0
- package/src/css/component/Table.module.scss +16 -0
- package/src/css/component/Tour.module.scss +108 -0
- package/src/css/component/VirtualScroller.module.scss +17 -0
- package/src/css/mixin/button-active.scss +3 -1
- package/src/data/di.ts +40 -0
- package/src/data/i18n.ts +7 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<FluxFadeTransition>
|
|
4
|
+
<div
|
|
5
|
+
v-if="active"
|
|
6
|
+
:class="$style.tour">
|
|
7
|
+
<div
|
|
8
|
+
v-if="targetRect"
|
|
9
|
+
:class="$style.tourSpotlight"
|
|
10
|
+
:style="{
|
|
11
|
+
'--x': `${targetRect.x - maskPadding}px`,
|
|
12
|
+
'--y': `${targetRect.y - maskPadding}px`,
|
|
13
|
+
'--w': `${targetRect.width + maskPadding * 2}px`,
|
|
14
|
+
'--h': `${targetRect.height + maskPadding * 2}px`
|
|
15
|
+
}"/>
|
|
16
|
+
|
|
17
|
+
<AnchorPopup
|
|
18
|
+
v-if="targetRect && currentItem"
|
|
19
|
+
ref="popup"
|
|
20
|
+
:anchor="virtualAnchor"
|
|
21
|
+
:class="clsx($style.tourPopover, isStepping && $style.isStepping)"
|
|
22
|
+
:position="currentItem.position ?? 'bottom'"
|
|
23
|
+
aria-modal="true"
|
|
24
|
+
role="dialog">
|
|
25
|
+
<FluxPane :class="$style.tourPane">
|
|
26
|
+
<div
|
|
27
|
+
ref="bodyViewport"
|
|
28
|
+
:class="$style.tourBodyViewport">
|
|
29
|
+
<Transition
|
|
30
|
+
@after-enter="onBodyAfterEnter"
|
|
31
|
+
@enter="onBodyEnter">
|
|
32
|
+
<div
|
|
33
|
+
:key="step"
|
|
34
|
+
:class="$style.tourBody">
|
|
35
|
+
<strong
|
|
36
|
+
v-if="currentItem.title"
|
|
37
|
+
:class="$style.tourTitle">
|
|
38
|
+
{{ currentItem.title }}
|
|
39
|
+
</strong>
|
|
40
|
+
|
|
41
|
+
<div :class="$style.tourContent">
|
|
42
|
+
<VNodeRenderer :vnode="currentContent"/>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</Transition>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div :class="$style.tourFooter">
|
|
49
|
+
<span :class="$style.tourProgress">
|
|
50
|
+
{{ step + 1 }} / {{ total }}
|
|
51
|
+
</span>
|
|
52
|
+
|
|
53
|
+
<FluxSpacer/>
|
|
54
|
+
|
|
55
|
+
<button
|
|
56
|
+
:class="$style.tourSkip"
|
|
57
|
+
type="button"
|
|
58
|
+
@click="skip">
|
|
59
|
+
{{ translate('flux.skip') }}
|
|
60
|
+
</button>
|
|
61
|
+
|
|
62
|
+
<FluxSecondaryButton
|
|
63
|
+
v-if="step > 0"
|
|
64
|
+
:aria-label="translate('flux.previous')"
|
|
65
|
+
icon-leading="angle-left"
|
|
66
|
+
size="small"
|
|
67
|
+
@click="previous"/>
|
|
68
|
+
|
|
69
|
+
<FluxPrimaryButton
|
|
70
|
+
v-if="step < total - 1"
|
|
71
|
+
:aria-label="translate('flux.next')"
|
|
72
|
+
icon-leading="angle-right"
|
|
73
|
+
size="small"
|
|
74
|
+
@click="next"/>
|
|
75
|
+
|
|
76
|
+
<FluxPrimaryButton
|
|
77
|
+
v-else
|
|
78
|
+
:aria-label="translate('flux.done')"
|
|
79
|
+
icon-leading="check"
|
|
80
|
+
size="small"
|
|
81
|
+
@click="next"/>
|
|
82
|
+
</div>
|
|
83
|
+
</FluxPane>
|
|
84
|
+
</AnchorPopup>
|
|
85
|
+
</div>
|
|
86
|
+
</FluxFadeTransition>
|
|
87
|
+
</Teleport>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<script
|
|
91
|
+
lang="ts"
|
|
92
|
+
setup>
|
|
93
|
+
import { isHtmlElement } from '@basmilius/utils';
|
|
94
|
+
import { flattenVNodeTree, isSSR, useEventListener } from '@flux-ui/internals';
|
|
95
|
+
import { clsx } from 'clsx';
|
|
96
|
+
import { type ComponentPublicInstance, computed, Fragment, h, nextTick, ref, useTemplateRef, type VNode, watch } from 'vue';
|
|
97
|
+
import { AnchorPopup, VNodeRenderer } from '~flux/components/component/primitive';
|
|
98
|
+
import { useTranslate } from '~flux/components/composable/private';
|
|
99
|
+
import { FluxFadeTransition } from '~flux/components/transition';
|
|
100
|
+
import FluxPane from './FluxPane.vue';
|
|
101
|
+
import FluxPrimaryButton from './FluxPrimaryButton.vue';
|
|
102
|
+
import FluxSecondaryButton from './FluxSecondaryButton.vue';
|
|
103
|
+
import FluxSpacer from './FluxSpacer.vue';
|
|
104
|
+
import FluxTourItem from './FluxTourItem.vue';
|
|
105
|
+
import $style from '~flux/components/css/component/Tour.module.scss';
|
|
106
|
+
|
|
107
|
+
type FluxTourPosition =
|
|
108
|
+
| 'top' | 'top-left' | 'top-right'
|
|
109
|
+
| 'left' | 'left-top' | 'left-bottom'
|
|
110
|
+
| 'right' | 'right-top' | 'right-bottom'
|
|
111
|
+
| 'bottom' | 'bottom-left' | 'bottom-right';
|
|
112
|
+
|
|
113
|
+
type TourItem = {
|
|
114
|
+
readonly target: string | (() => HTMLElement | null);
|
|
115
|
+
readonly title?: string;
|
|
116
|
+
readonly position?: FluxTourPosition;
|
|
117
|
+
readonly content?: () => VNode[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const active = defineModel<boolean>('active', {
|
|
121
|
+
required: true
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const step = defineModel<number>('step', {
|
|
125
|
+
default: 0
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
maskPadding = 8,
|
|
130
|
+
root
|
|
131
|
+
} = defineProps<{
|
|
132
|
+
readonly maskPadding?: number;
|
|
133
|
+
readonly root?: string | HTMLElement | (() => HTMLElement | null);
|
|
134
|
+
}>();
|
|
135
|
+
|
|
136
|
+
const emit = defineEmits<{
|
|
137
|
+
finish: [];
|
|
138
|
+
skip: [];
|
|
139
|
+
next: [number];
|
|
140
|
+
prev: [number];
|
|
141
|
+
}>();
|
|
142
|
+
|
|
143
|
+
const slots = defineSlots<{
|
|
144
|
+
default(): VNode[];
|
|
145
|
+
}>();
|
|
146
|
+
|
|
147
|
+
const translate = useTranslate();
|
|
148
|
+
|
|
149
|
+
const popup = useTemplateRef<{ reposition(): void; resize(): void }>('popup');
|
|
150
|
+
const bodyViewport = useTemplateRef<HTMLElement>('bodyViewport');
|
|
151
|
+
const targetRect = ref<DOMRect | null>(null);
|
|
152
|
+
const isStepping = ref(false);
|
|
153
|
+
|
|
154
|
+
let steppingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
155
|
+
|
|
156
|
+
const items = computed<readonly TourItem[]>(() => {
|
|
157
|
+
const vnodes = flattenVNodeTree(slots.default?.() ?? []);
|
|
158
|
+
const out: TourItem[] = [];
|
|
159
|
+
|
|
160
|
+
for (const vnode of vnodes) {
|
|
161
|
+
if (vnode.type !== FluxTourItem) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const props = vnode.props ?? {};
|
|
166
|
+
const children = vnode.children as { default?: () => VNode[] } | null;
|
|
167
|
+
|
|
168
|
+
out.push({
|
|
169
|
+
target: props.target,
|
|
170
|
+
title: props.title,
|
|
171
|
+
position: props.position,
|
|
172
|
+
content: children?.default
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return out;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const total = computed(() => items.value.length);
|
|
180
|
+
const currentItem = computed(() => items.value[step.value]);
|
|
181
|
+
const currentContent = computed(() => {
|
|
182
|
+
const content = currentItem.value?.content?.();
|
|
183
|
+
return content ? h(Fragment, content) : null;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const virtualAnchor = {
|
|
187
|
+
$el: {
|
|
188
|
+
getBoundingClientRect: () => {
|
|
189
|
+
const rect = targetRect.value;
|
|
190
|
+
|
|
191
|
+
if (!rect) {
|
|
192
|
+
return new DOMRect(0, 0, 0, 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new DOMRect(rect.x - maskPadding, rect.y - maskPadding, rect.width + maskPadding * 2, rect.height + maskPadding * 2);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} as unknown as ComponentPublicInstance;
|
|
199
|
+
|
|
200
|
+
function resolveScope(): ParentNode {
|
|
201
|
+
if (!root) {
|
|
202
|
+
return document;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof root === 'string') {
|
|
206
|
+
return document.querySelector(root) ?? document;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof root === 'function') {
|
|
210
|
+
return root() ?? document;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return root;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveTarget(): HTMLElement | null {
|
|
217
|
+
const current = items.value[step.value];
|
|
218
|
+
|
|
219
|
+
if (!current) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return typeof current.target === 'function' ? current.target() : resolveScope().querySelector<HTMLElement>(current.target);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function measure(): void {
|
|
227
|
+
const element = resolveTarget();
|
|
228
|
+
|
|
229
|
+
if (!element) {
|
|
230
|
+
targetRect.value = null;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
element.scrollIntoView({block: 'center', inline: 'center'});
|
|
235
|
+
|
|
236
|
+
requestAnimationFrame(() => {
|
|
237
|
+
const resolved = resolveTarget();
|
|
238
|
+
targetRect.value = resolved ? resolved.getBoundingClientRect() : null;
|
|
239
|
+
popup.value?.reposition();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function next(): void {
|
|
244
|
+
if (step.value < total.value - 1) {
|
|
245
|
+
step.value++;
|
|
246
|
+
emit('next', step.value);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
finish();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function previous(): void {
|
|
254
|
+
if (step.value > 0) {
|
|
255
|
+
step.value--;
|
|
256
|
+
emit('prev', step.value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function skip(): void {
|
|
261
|
+
active.value = false;
|
|
262
|
+
emit('skip');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function finish(): void {
|
|
266
|
+
active.value = false;
|
|
267
|
+
emit('finish');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function onBodyEnter(el: Element): void {
|
|
271
|
+
if (!isHtmlElement(el) || !bodyViewport.value) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const height = el.offsetHeight;
|
|
276
|
+
|
|
277
|
+
requestAnimationFrame(() => {
|
|
278
|
+
if (bodyViewport.value) {
|
|
279
|
+
bodyViewport.value.style.height = `${height}px`;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function onBodyAfterEnter(): void {
|
|
285
|
+
if (bodyViewport.value) {
|
|
286
|
+
bodyViewport.value.style.height = 'auto';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
watch(step, (newStep, oldStep) => {
|
|
291
|
+
if (active.value && newStep !== oldStep) {
|
|
292
|
+
if (bodyViewport.value) {
|
|
293
|
+
bodyViewport.value.style.height = `${bodyViewport.value.offsetHeight}px`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
isStepping.value = true;
|
|
297
|
+
clearTimeout(steppingTimer);
|
|
298
|
+
steppingTimer = setTimeout(() => {
|
|
299
|
+
isStepping.value = false;
|
|
300
|
+
}, 300);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
watch([active, step], async () => {
|
|
305
|
+
if (!active.value) {
|
|
306
|
+
targetRect.value = null;
|
|
307
|
+
isStepping.value = false;
|
|
308
|
+
clearTimeout(steppingTimer);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await nextTick();
|
|
313
|
+
measure();
|
|
314
|
+
}, {immediate: true});
|
|
315
|
+
|
|
316
|
+
if (!isSSR) {
|
|
317
|
+
const onRemeasure = () => {
|
|
318
|
+
if (active.value && targetRect.value) {
|
|
319
|
+
const resolved = resolveTarget();
|
|
320
|
+
targetRect.value = resolved ? resolved.getBoundingClientRect() : null;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
useEventListener(ref(window), 'resize', onRemeasure);
|
|
325
|
+
useEventListener(ref(window), 'scroll', onRemeasure, {capture: true, passive: true});
|
|
326
|
+
useEventListener(ref(window), 'keydown', (evt: KeyboardEvent) => {
|
|
327
|
+
if (active.value && evt.key === 'Escape') {
|
|
328
|
+
skip();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script
|
|
2
|
+
lang="ts"
|
|
3
|
+
setup>
|
|
4
|
+
import type { VNode } from 'vue';
|
|
5
|
+
|
|
6
|
+
type FluxTourPosition =
|
|
7
|
+
| 'top' | 'top-left' | 'top-right'
|
|
8
|
+
| 'left' | 'left-top' | 'left-bottom'
|
|
9
|
+
| 'right' | 'right-top' | 'right-bottom'
|
|
10
|
+
| 'bottom' | 'bottom-left' | 'bottom-right';
|
|
11
|
+
|
|
12
|
+
defineProps<{
|
|
13
|
+
readonly target: string | (() => HTMLElement | null);
|
|
14
|
+
readonly title?: string;
|
|
15
|
+
readonly position?: FluxTourPosition;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
defineSlots<{
|
|
19
|
+
default(): VNode[];
|
|
20
|
+
}>();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<span
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
style="display: none"/>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="viewport"
|
|
4
|
+
:class="$style.virtualScroller">
|
|
5
|
+
<div
|
|
6
|
+
:class="$style.virtualScrollerSpacer"
|
|
7
|
+
:style="{
|
|
8
|
+
height: `${totalHeight}px`
|
|
9
|
+
}">
|
|
10
|
+
<div
|
|
11
|
+
:class="$style.virtualScrollerWindow"
|
|
12
|
+
:style="{
|
|
13
|
+
transform: `translateY(${offsetY}px)`
|
|
14
|
+
}">
|
|
15
|
+
<div
|
|
16
|
+
v-for="entry of visibleEntries"
|
|
17
|
+
:key="entry.index"
|
|
18
|
+
:style="{
|
|
19
|
+
height: `${estimatedItemHeight}px`
|
|
20
|
+
}">
|
|
21
|
+
<slot
|
|
22
|
+
:index="entry.index"
|
|
23
|
+
:item="entry.item"/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script
|
|
31
|
+
lang="ts"
|
|
32
|
+
setup
|
|
33
|
+
generic="T">
|
|
34
|
+
import { computed, onMounted, onUnmounted, ref, useTemplateRef, type VNode } from 'vue';
|
|
35
|
+
import { useScrollPosition } from '@flux-ui/internals';
|
|
36
|
+
import $style from '~flux/components/css/component/VirtualScroller.module.scss';
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
estimatedItemHeight = 40,
|
|
40
|
+
items,
|
|
41
|
+
overscan = 4
|
|
42
|
+
} = defineProps<{
|
|
43
|
+
readonly estimatedItemHeight?: number;
|
|
44
|
+
readonly items: T[];
|
|
45
|
+
readonly overscan?: number;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
defineSlots<{
|
|
49
|
+
default(props: {readonly index: number; readonly item: T}): VNode[];
|
|
50
|
+
}>();
|
|
51
|
+
|
|
52
|
+
const viewport = useTemplateRef<HTMLElement>('viewport');
|
|
53
|
+
const {y} = useScrollPosition(viewport);
|
|
54
|
+
|
|
55
|
+
const viewportHeight = ref(0);
|
|
56
|
+
|
|
57
|
+
const totalHeight = computed(() => items.length * estimatedItemHeight);
|
|
58
|
+
const startIndex = computed(() => Math.max(0, Math.floor(y.value / estimatedItemHeight) - overscan));
|
|
59
|
+
const endIndex = computed(() => Math.min(items.length, Math.ceil((y.value + viewportHeight.value) / estimatedItemHeight) + overscan));
|
|
60
|
+
const offsetY = computed(() => startIndex.value * estimatedItemHeight);
|
|
61
|
+
|
|
62
|
+
const visibleEntries = computed(() => {
|
|
63
|
+
const entries: {index: number; item: T}[] = [];
|
|
64
|
+
|
|
65
|
+
for (let index = startIndex.value; index < endIndex.value; ++index) {
|
|
66
|
+
entries.push({index, item: items[index]});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return entries;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let observer: ResizeObserver | null = null;
|
|
73
|
+
|
|
74
|
+
onMounted(() => {
|
|
75
|
+
const element = viewport.value;
|
|
76
|
+
|
|
77
|
+
if (!element) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
viewportHeight.value = element.clientHeight;
|
|
82
|
+
|
|
83
|
+
observer = new ResizeObserver(() => {
|
|
84
|
+
if (viewport.value) {
|
|
85
|
+
viewportHeight.value = viewport.value.clientHeight;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
observer.observe(element);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
onUnmounted(() => {
|
|
93
|
+
observer?.disconnect();
|
|
94
|
+
observer = null;
|
|
95
|
+
});
|
|
96
|
+
</script>
|
package/src/component/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as FluxAdaptiveSlot } from './FluxAdaptiveSlot.vue';
|
|
|
7
7
|
export { default as FluxAnimatedColors } from './FluxAnimatedColors.vue';
|
|
8
8
|
export { default as FluxAspectRatio } from './FluxAspectRatio.vue';
|
|
9
9
|
export { default as FluxAvatar } from './FluxAvatar.vue';
|
|
10
|
+
export { default as FluxAvatarGroup } from './FluxAvatarGroup.vue';
|
|
10
11
|
export { default as FluxBadge } from './FluxBadge.vue';
|
|
11
12
|
export { default as FluxBadgeStack } from './FluxBadgeStack.vue';
|
|
12
13
|
export { default as FluxBorderBeam } from './FluxBorderBeam.vue';
|
|
@@ -28,8 +29,11 @@ export { default as FluxComment } from './FluxComment.vue';
|
|
|
28
29
|
export { default as FluxColorPicker } from './FluxColorPicker.vue';
|
|
29
30
|
export { default as FluxColorSelect } from './FluxColorSelect.vue';
|
|
30
31
|
export { default as FluxContainer } from './FluxContainer.vue';
|
|
32
|
+
export { default as FluxContextMenu } from './FluxContextMenu.vue';
|
|
31
33
|
export { default as FluxDataTable } from './FluxDataTable.vue';
|
|
32
34
|
export { default as FluxDatePicker } from './FluxDatePicker.vue';
|
|
35
|
+
export { default as FluxDescriptionItem } from './FluxDescriptionItem.vue';
|
|
36
|
+
export { default as FluxDescriptionList } from './FluxDescriptionList.vue';
|
|
33
37
|
export { default as FluxDestructiveButton } from './FluxDestructiveButton.vue';
|
|
34
38
|
export { default as FluxDisabled } from './FluxDisabled.vue';
|
|
35
39
|
export { default as FluxDivider } from './FluxDivider.vue';
|
|
@@ -59,6 +63,7 @@ export { default as FluxForm } from './FluxForm.vue';
|
|
|
59
63
|
export { default as FluxFormCheckbox } from './FluxFormCheckbox.vue';
|
|
60
64
|
export { default as FluxFormCheckboxGroup } from './FluxFormCheckboxGroup.vue';
|
|
61
65
|
export { default as FluxFormColumn } from './FluxFormColumn.vue';
|
|
66
|
+
export { default as FluxFormCombobox } from './FluxFormCombobox.vue';
|
|
62
67
|
export { default as FluxFormDateInput } from './FluxFormDateInput.vue';
|
|
63
68
|
export { default as FluxFormDateRangeInput } from './FluxFormDateRangeInput.vue';
|
|
64
69
|
export { default as FluxFormDateTimeInput } from './FluxFormDateTimeInput.vue';
|
|
@@ -73,11 +78,13 @@ export { default as FluxFormPinInput } from './FluxFormPinInput.vue';
|
|
|
73
78
|
export { default as FluxFormRadio } from './FluxFormRadio.vue';
|
|
74
79
|
export { default as FluxFormRadioGroup } from './FluxFormRadioGroup.vue';
|
|
75
80
|
export { default as FluxFormRangeSlider } from './FluxFormRangeSlider.vue';
|
|
81
|
+
export { default as FluxFormRating } from './FluxFormRating.vue';
|
|
76
82
|
export { default as FluxFormRow } from './FluxFormRow.vue';
|
|
77
83
|
export { default as FluxFormSection } from './FluxFormSection.vue';
|
|
78
84
|
export { default as FluxFormSelect } from './FluxFormSelect.vue';
|
|
79
85
|
export { default as FluxFormSelectAsync } from './FluxFormSelectAsync.vue';
|
|
80
86
|
export { default as FluxFormSlider } from './FluxFormSlider.vue';
|
|
87
|
+
export { default as FluxFormTagsInput } from './FluxFormTagsInput.vue';
|
|
81
88
|
export { default as FluxFormTextArea } from './FluxFormTextArea.vue';
|
|
82
89
|
export { default as FluxFormTimeZonePicker } from './FluxFormTimeZonePicker.vue';
|
|
83
90
|
export { default as FluxFormTreeViewSelect } from './FluxFormTreeViewSelect.vue';
|
|
@@ -88,6 +95,7 @@ export { default as FluxGridColumn } from './FluxGridColumn.vue';
|
|
|
88
95
|
export { default as FluxGridPattern } from './FluxGridPattern.vue';
|
|
89
96
|
export { default as FluxIcon } from './FluxIcon.vue';
|
|
90
97
|
export { default as FluxInfo } from './FluxInfo.vue';
|
|
98
|
+
export { default as FluxInlineEdit } from './FluxInlineEdit.vue';
|
|
91
99
|
export { default as FluxKanban } from './FluxKanban.vue';
|
|
92
100
|
export { default as FluxKanbanColumn } from './FluxKanbanColumn.vue';
|
|
93
101
|
export { default as FluxKanbanItem } from './FluxKanbanItem.vue';
|
|
@@ -102,6 +110,7 @@ export { default as FluxLayerPaneSecondary } from './FluxLayerPaneSecondary.vue'
|
|
|
102
110
|
export { default as FluxLink } from './FluxLink.vue';
|
|
103
111
|
export { default as FluxMenu } from './FluxMenu.vue';
|
|
104
112
|
export { default as FluxMenuCollapsible } from './FluxMenuCollapsible.vue';
|
|
113
|
+
export { default as FluxMenuFlyout } from './FluxMenuFlyout.vue';
|
|
105
114
|
export { default as FluxMenuGroup } from './FluxMenuGroup.vue';
|
|
106
115
|
export { default as FluxMenuItem } from './FluxMenuItem.vue';
|
|
107
116
|
export { default as FluxMenuOptions } from './FluxMenuOptions.vue';
|
|
@@ -171,5 +180,8 @@ export { default as FluxToolbar } from './FluxToolbar.vue';
|
|
|
171
180
|
export { default as FluxToolbarGroup } from './FluxToolbarGroup.vue';
|
|
172
181
|
export { default as FluxTooltip } from './FluxTooltip.vue';
|
|
173
182
|
export { default as FluxTooltipProvider } from './FluxTooltipProvider.vue';
|
|
183
|
+
export { default as FluxTour } from './FluxTour.vue';
|
|
184
|
+
export { default as FluxTourItem } from './FluxTourItem.vue';
|
|
174
185
|
export { default as FluxTreeView } from './FluxTreeView.vue';
|
|
186
|
+
export { default as FluxVirtualScroller } from './FluxVirtualScroller.vue';
|
|
175
187
|
export { default as FluxWindow } from './FluxWindow.vue';
|
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
|
|
21
21
|
const {
|
|
22
22
|
anchor,
|
|
23
|
+
clampToViewport,
|
|
23
24
|
direction = 'vertical',
|
|
24
25
|
margin = 12,
|
|
25
26
|
position,
|
|
26
27
|
useAnchorWidth
|
|
27
28
|
} = defineProps<{
|
|
28
29
|
readonly anchor?: ComponentPublicInstance | HTMLElement | null;
|
|
30
|
+
readonly clampToViewport?: boolean;
|
|
29
31
|
readonly direction?: FluxDirection;
|
|
30
32
|
readonly margin?: number;
|
|
31
33
|
readonly position?:
|
|
@@ -52,6 +54,9 @@
|
|
|
52
54
|
onMounted(() => {
|
|
53
55
|
window.addEventListener('resize', onResize, {passive: true});
|
|
54
56
|
window.addEventListener('scroll', onScroll, {capture: true, passive: true});
|
|
57
|
+
|
|
58
|
+
resize();
|
|
59
|
+
reposition();
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
onUnmounted(() => {
|
|
@@ -161,6 +166,23 @@
|
|
|
161
166
|
break;
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
if (clampToViewport) {
|
|
170
|
+
if (position?.startsWith('right') && px + popupWidth > innerWidth - margin) {
|
|
171
|
+
px = x - popupWidth - margin;
|
|
172
|
+
} else if (position?.startsWith('left') && px < margin) {
|
|
173
|
+
px = x + width + margin;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (position?.startsWith('bottom') && py + popupHeight > innerHeight - margin) {
|
|
177
|
+
py = y - popupHeight - margin;
|
|
178
|
+
} else if (position?.startsWith('top') && py < margin) {
|
|
179
|
+
py = y + height + margin;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
px = Math.min(Math.max(px, margin), Math.max(margin, innerWidth - popupWidth - margin));
|
|
183
|
+
py = Math.min(Math.max(py, margin), Math.max(margin, innerHeight - popupHeight - margin));
|
|
184
|
+
}
|
|
185
|
+
|
|
164
186
|
state.x = px;
|
|
165
187
|
state.y = py;
|
|
166
188
|
}
|
|
@@ -185,6 +207,11 @@
|
|
|
185
207
|
reposition();
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
defineExpose({
|
|
211
|
+
reposition,
|
|
212
|
+
resize
|
|
213
|
+
});
|
|
214
|
+
|
|
188
215
|
watchEffect(() => {
|
|
189
216
|
if (!anchor || (!isHtmlElement(anchor) && !anchor.$el)) {
|
|
190
217
|
return;
|
|
@@ -80,11 +80,19 @@
|
|
|
80
80
|
:placeholder="translate('flux.search')"
|
|
81
81
|
@keydown="onKeyDown"/>
|
|
82
82
|
|
|
83
|
-
<FluxMenu v-if="
|
|
83
|
+
<FluxMenu v-if="canCreate">
|
|
84
|
+
<FluxMenuItem
|
|
85
|
+
icon-leading="plus"
|
|
86
|
+
:label="translate('flux.createOption', {value: trimmedSearch})"
|
|
87
|
+
type="button"
|
|
88
|
+
@click="create()"/>
|
|
89
|
+
</FluxMenu>
|
|
90
|
+
|
|
91
|
+
<FluxMenu v-if="!isLoading && options.length === 0 && !canCreate">
|
|
84
92
|
<FluxMenuSubHeader :label="translate('flux.noItems')"/>
|
|
85
93
|
</FluxMenu>
|
|
86
94
|
|
|
87
|
-
<FluxMenu v-else>
|
|
95
|
+
<FluxMenu v-else-if="options.length > 0">
|
|
88
96
|
<template
|
|
89
97
|
v-for="([item, subItems], index) of options"
|
|
90
98
|
:key="`group-${index}`">
|
|
@@ -162,6 +170,7 @@
|
|
|
162
170
|
keyDown: [KeyboardEvent];
|
|
163
171
|
deselect: [string | number | null];
|
|
164
172
|
select: [string | number | null];
|
|
173
|
+
create: [string];
|
|
165
174
|
search: [string];
|
|
166
175
|
close: [];
|
|
167
176
|
open: [];
|
|
@@ -177,11 +186,13 @@
|
|
|
177
186
|
|
|
178
187
|
const {
|
|
179
188
|
disabled: componentDisabled,
|
|
189
|
+
isCreatable,
|
|
180
190
|
isMultiple,
|
|
181
191
|
options,
|
|
182
192
|
selected
|
|
183
193
|
} = defineProps<{
|
|
184
194
|
readonly disabled?: boolean;
|
|
195
|
+
readonly isCreatable?: boolean;
|
|
185
196
|
readonly isLoading?: boolean;
|
|
186
197
|
readonly isMultiple?: boolean;
|
|
187
198
|
readonly isSearchable?: boolean;
|
|
@@ -205,6 +216,15 @@
|
|
|
205
216
|
const focusElement = computed(() => unrefTemplateElement(searchInputElementRef) ?? unrefTemplateElement(anchorRef));
|
|
206
217
|
const highlightedId = computed(() => unref(rawOptions)[unref(highlightedIndex)]?.value);
|
|
207
218
|
const rawOptions = computed(() => options.map(group => group[1]).flat());
|
|
219
|
+
const trimmedSearch = computed(() => unref(modelSearch).trim());
|
|
220
|
+
const canCreate = computed(() => {
|
|
221
|
+
if (!isCreatable || trimmedSearch.value === '') {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const query = trimmedSearch.value.toLowerCase();
|
|
226
|
+
return !unref(rawOptions).some(o => o.label.toLowerCase() === query);
|
|
227
|
+
});
|
|
208
228
|
|
|
209
229
|
const {
|
|
210
230
|
isOpen: isPopupOpen,
|
|
@@ -236,6 +256,19 @@
|
|
|
236
256
|
nextTick(() => unref(focusElement)?.focus());
|
|
237
257
|
}
|
|
238
258
|
|
|
259
|
+
function create(): void {
|
|
260
|
+
emit('create', trimmedSearch.value);
|
|
261
|
+
|
|
262
|
+
if (!isMultiple) {
|
|
263
|
+
isPopupOpen.value = false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
highlightedIndex.value = INITIAL_HIGHLIGHTED_INDEX;
|
|
267
|
+
modelSearch.value = '';
|
|
268
|
+
|
|
269
|
+
nextTick(() => unref(focusElement)?.focus());
|
|
270
|
+
}
|
|
271
|
+
|
|
239
272
|
function onKeyDown(evt: KeyboardEvent): void {
|
|
240
273
|
emit('keyDown', evt);
|
|
241
274
|
|
|
@@ -284,6 +317,8 @@
|
|
|
284
317
|
|
|
285
318
|
if (id !== undefined) {
|
|
286
319
|
select(id);
|
|
320
|
+
} else if (unref(canCreate)) {
|
|
321
|
+
create();
|
|
287
322
|
}
|
|
288
323
|
break;
|
|
289
324
|
|
|
@@ -4,6 +4,7 @@ export { default as useDateFlyout } from './useDateFlyout';
|
|
|
4
4
|
export { default as useDropdownPopup, type UseDropdownPopupOptions, type UseDropdownPopupReturn } from './useDropdownPopup';
|
|
5
5
|
export { default as useFormSelect } from './useFormSelect';
|
|
6
6
|
export { useKanban } from './useKanban';
|
|
7
|
+
export { default as useMenuFlyout, useMenuFlyoutContext, useMenuFlyoutProvider, type UseMenuFlyoutOptions, type UseMenuFlyoutProviderOptions, type UseMenuFlyoutReturn } from './useMenuFlyout';
|
|
7
8
|
export { useSplitView, type SplitViewPane, type UseSplitViewOptions, type UseSplitViewReturn } from './useSplitView';
|
|
8
9
|
export { default as useTranslate } from './useTranslate';
|
|
9
10
|
export { useCommandPalette, type CommandPaletteGroup, type CommandPaletteResultItem } from './useCommandPalette';
|