@flux-ui/components 3.1.2 → 3.1.3
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/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/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 +441 -1
- package/dist/index.js +2018 -407
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/component/FluxAvatarGroup.vue +52 -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/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/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/data/di.ts +40 -0
- package/src/data/i18n.ts +7 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FluxMenuItem
|
|
3
|
+
ref="trigger"
|
|
4
|
+
:class="isOpen && $style.menuFlyoutTriggerOpen"
|
|
5
|
+
command-icon="angle-right"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:icon-leading="icon"
|
|
8
|
+
:is-active="isActive"
|
|
9
|
+
:is-destructive="isDestructive"
|
|
10
|
+
:label="label"
|
|
11
|
+
aria-haspopup="menu"
|
|
12
|
+
:aria-expanded="isOpen ? 'true' : 'false'"
|
|
13
|
+
@click="onTriggerClick"
|
|
14
|
+
@keydown="onTriggerKeydown">
|
|
15
|
+
<template
|
|
16
|
+
v-if="$slots.trigger"
|
|
17
|
+
#label>
|
|
18
|
+
<slot name="trigger"/>
|
|
19
|
+
</template>
|
|
20
|
+
</FluxMenuItem>
|
|
21
|
+
|
|
22
|
+
<Teleport to="body">
|
|
23
|
+
<AnchorPopup
|
|
24
|
+
v-if="isOpen"
|
|
25
|
+
ref="popup"
|
|
26
|
+
:anchor="popupAnchor"
|
|
27
|
+
:class="$style.menuFlyoutPopup"
|
|
28
|
+
clamp-to-viewport
|
|
29
|
+
:margin="2"
|
|
30
|
+
:position="position"
|
|
31
|
+
role="menu"
|
|
32
|
+
:aria-label="translate('flux.submenu')"
|
|
33
|
+
@keydown="onPopupKeydown">
|
|
34
|
+
<slot/>
|
|
35
|
+
</AnchorPopup>
|
|
36
|
+
|
|
37
|
+
<svg
|
|
38
|
+
v-if="showDebugCone"
|
|
39
|
+
:class="$style.menuFlyoutConeDebug">
|
|
40
|
+
<polygon :points="conePoints"/>
|
|
41
|
+
<circle
|
|
42
|
+
:cx="coneApex.x"
|
|
43
|
+
:cy="coneApex.y"
|
|
44
|
+
r="4"/>
|
|
45
|
+
</svg>
|
|
46
|
+
</Teleport>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script
|
|
50
|
+
lang="ts"
|
|
51
|
+
setup>
|
|
52
|
+
import type { FluxIconName } from '@flux-ui/types';
|
|
53
|
+
import { computed, type ComponentPublicInstance, toRef, useTemplateRef, type VNode } from 'vue';
|
|
54
|
+
import { useMenuFlyout, useTranslate } from '~flux/components/composable/private';
|
|
55
|
+
import { AnchorPopup } from '~flux/components/component/primitive';
|
|
56
|
+
import FluxMenuItem from './FluxMenuItem.vue';
|
|
57
|
+
import $style from '~flux/components/css/component/MenuFlyout.module.scss';
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
disabled,
|
|
61
|
+
position = 'right-top'
|
|
62
|
+
} = defineProps<{
|
|
63
|
+
readonly disabled?: boolean;
|
|
64
|
+
readonly icon?: FluxIconName;
|
|
65
|
+
readonly isActive?: boolean;
|
|
66
|
+
readonly isDestructive?: boolean;
|
|
67
|
+
readonly label?: string;
|
|
68
|
+
readonly position?:
|
|
69
|
+
| 'top' | 'top-left' | 'top-right'
|
|
70
|
+
| 'left' | 'left-top' | 'left-bottom'
|
|
71
|
+
| 'right' | 'right-top' | 'right-bottom'
|
|
72
|
+
| 'bottom' | 'bottom-left' | 'bottom-right';
|
|
73
|
+
}>();
|
|
74
|
+
|
|
75
|
+
defineSlots<{
|
|
76
|
+
default(): VNode[];
|
|
77
|
+
trigger(): VNode[];
|
|
78
|
+
}>();
|
|
79
|
+
|
|
80
|
+
// The top chrome of a menu inside a pane: 1px pane border + 9px menu margin
|
|
81
|
+
// (see `.basePane > .menu` in Menu.module.scss). Subtracting it aligns the first
|
|
82
|
+
// submenu item with the opener item instead of dropping it lower.
|
|
83
|
+
const MENU_CHROME_TOP = 10;
|
|
84
|
+
|
|
85
|
+
const translate = useTranslate();
|
|
86
|
+
|
|
87
|
+
const triggerRef = useTemplateRef<ComponentPublicInstance>('trigger');
|
|
88
|
+
const popupRef = useTemplateRef<ComponentPublicInstance>('popup');
|
|
89
|
+
|
|
90
|
+
const popupAnchor = {
|
|
91
|
+
$el: {
|
|
92
|
+
getBoundingClientRect(): DOMRect {
|
|
93
|
+
const element = triggerRef.value?.$el as HTMLElement | undefined;
|
|
94
|
+
|
|
95
|
+
if (!element) {
|
|
96
|
+
return new DOMRect();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const rect = element.getBoundingClientRect();
|
|
100
|
+
|
|
101
|
+
return new DOMRect(rect.x, rect.y - MENU_CHROME_TOP, rect.width, rect.height);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} as unknown as ComponentPublicInstance;
|
|
105
|
+
|
|
106
|
+
const {
|
|
107
|
+
context,
|
|
108
|
+
cone,
|
|
109
|
+
isOpen,
|
|
110
|
+
onPopupKeydown,
|
|
111
|
+
onTriggerClick,
|
|
112
|
+
onTriggerKeydown
|
|
113
|
+
} = useMenuFlyout({triggerRef, popupRef, disabled: toRef(() => disabled)});
|
|
114
|
+
|
|
115
|
+
const showDebugCone = computed(() => !!context && context.debugCone.value && isOpen.value && !!cone.value);
|
|
116
|
+
const conePoints = computed(() => cone.value ? `${cone.value.ax},${cone.value.ay} ${cone.value.bx},${cone.value.by} ${cone.value.cx},${cone.value.cy}` : '');
|
|
117
|
+
const coneApex = computed(() => ({x: cone.value?.ax ?? 0, y: cone.value?.ay ?? 0}));
|
|
118
|
+
</script>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
isSeparated && $style.isSeparated,
|
|
8
8
|
isStriped && $style.isStriped
|
|
9
9
|
)"
|
|
10
|
+
:colspan="colspan"
|
|
10
11
|
role="cell">
|
|
11
12
|
<slot name="content">
|
|
12
13
|
<div
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
const {
|
|
33
34
|
contentDirection = 'row'
|
|
34
35
|
} = defineProps<{
|
|
36
|
+
readonly colspan?: number;
|
|
35
37
|
readonly contentDirection?: 'column' | 'row';
|
|
36
38
|
readonly contentGap?: number;
|
|
37
39
|
}>();
|
|
@@ -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';
|