@veltra/compositions 1.0.8 → 1.0.9

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/index.d.ts CHANGED
@@ -4,11 +4,11 @@ import { useDrag } from "./use-drag/index.js";
4
4
  import { useFallbackProps, useFormFallbackProps } from "./use-fallback-props/index.js";
5
5
  import { useFocus } from "./use-focus/index.js";
6
6
  import { useFormComponent } from "./use-form-component/index.js";
7
- import { Updater, useUpdateLock } from "./use-lock/index.js";
7
+ import { UserAction, UserActionResult, useUserAction } from "./use-user-action/index.js";
8
8
  import { useModel } from "./use-model/index.js";
9
9
  import { usePop } from "./use-pop/index.js";
10
10
  import { RefElement, ResizeObserverReturn, useObserverCallback, useResizeObserver } from "./use-resize-observer/index.js";
11
11
  import { useReactiveSize } from "./use-reactive-size/index.js";
12
12
  import { useTransition } from "./use-transition/index.js";
13
- import { VirtualReturned, useVirtual } from "./use-virtual/index.js";
14
- export { RefElement, ResizeObserverReturn, Updater, VirtualReturned, setDocumentSize, useComponentProps, useConfig, useDrag, useFallbackProps, useFocus, useFormComponent, useFormFallbackProps, useModel, useObserverCallback, usePop, useReactiveSize, useResizeObserver, useTransition, useUpdateLock, useVirtual };
13
+ import { EstimateSize, GetItemKey, UseVirtualizerOptions, UseVirtualizerReturned, VirtualAlign, VirtualItem, VirtualMeasurement, VirtualRange, VirtualScrollOptions, VirtualSnapshot, Virtualizer, VirtualizerOptions, useVirtualizer } from "./use-virtualizer/index.js";
14
+ export { EstimateSize, GetItemKey, RefElement, ResizeObserverReturn, UseVirtualizerOptions, UseVirtualizerReturned, UserAction, UserActionResult, VirtualAlign, VirtualItem, VirtualMeasurement, VirtualRange, VirtualScrollOptions, VirtualSnapshot, Virtualizer, VirtualizerOptions, setDocumentSize, useComponentProps, useConfig, useDrag, useFallbackProps, useFocus, useFormComponent, useFormFallbackProps, useModel, useObserverCallback, usePop, useReactiveSize, useResizeObserver, useTransition, useUserAction, useVirtualizer };
package/dist/index.js CHANGED
@@ -4,11 +4,11 @@ import { useDrag } from "./use-drag/index.js";
4
4
  import { useFallbackProps, useFormFallbackProps } from "./use-fallback-props/index.js";
5
5
  import { useFocus } from "./use-focus/index.js";
6
6
  import { useFormComponent } from "./use-form-component/index.js";
7
- import { useUpdateLock } from "./use-lock/index.js";
7
+ import { useUserAction } from "./use-user-action/index.js";
8
8
  import { useModel } from "./use-model/index.js";
9
9
  import { usePop } from "./use-pop/index.js";
10
10
  import { useObserverCallback, useResizeObserver } from "./use-resize-observer/index.js";
11
11
  import { useReactiveSize } from "./use-reactive-size/index.js";
12
12
  import { useTransition } from "./use-transition/index.js";
13
- import { useVirtual } from "./use-virtual/index.js";
14
- export { setDocumentSize, useComponentProps, useConfig, useDrag, useFallbackProps, useFocus, useFormComponent, useFormFallbackProps, useModel, useObserverCallback, usePop, useReactiveSize, useResizeObserver, useTransition, useUpdateLock, useVirtual };
13
+ import { useVirtualizer } from "./use-virtualizer/index.js";
14
+ export { setDocumentSize, useComponentProps, useConfig, useDrag, useFallbackProps, useFocus, useFormComponent, useFormFallbackProps, useModel, useObserverCallback, usePop, useReactiveSize, useResizeObserver, useTransition, useUserAction, useVirtualizer };
@@ -0,0 +1,32 @@
1
+ //#region src/use-user-action/index.d.ts
2
+ type UserAction = <T extends (...args: any[]) => void | Promise<void>>(fn: T) => (...args: Parameters<T>) => Promise<void>;
3
+ interface UserActionResult {
4
+ isUserActive: () => boolean;
5
+ userAction: UserAction;
6
+ }
7
+ /**
8
+ * 创建用户活动
9
+ * @description
10
+ * 解决"组件内部 emit 更新 → 外部 props 回传 → 监听 props 的副作用再次触发
11
+ * 内部状态变更"的循环更新问题。典型搭配:
12
+ *
13
+ * ```ts
14
+ * const { userAction, isUserActive } = useUserAction()
15
+ *
16
+ * // 将函数标记为一个用户动作
17
+ * const handleSelect = userAction((date: Dater) => {
18
+ * currentDate.value = date
19
+ * emit('update:modelValue', date.format(fmt))
20
+ * })
21
+ *
22
+ * // modelValue 回显:用户活动期间跳过
23
+ * watch(() => props.modelValue, (v) => {
24
+ * if (isUserActive()) return
25
+ * currentDate.value = v ? date(v) : undefined
26
+ * })
27
+ * ```
28
+ */
29
+ declare function useUserAction(): UserActionResult;
30
+ //#endregion
31
+ export { UserAction, UserActionResult, useUserAction };
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { nextTick } from "vue";
2
+ //#region src/use-user-action/index.ts
3
+ /**
4
+ * 创建用户活动
5
+ * @description
6
+ * 解决"组件内部 emit 更新 → 外部 props 回传 → 监听 props 的副作用再次触发
7
+ * 内部状态变更"的循环更新问题。典型搭配:
8
+ *
9
+ * ```ts
10
+ * const { userAction, isUserActive } = useUserAction()
11
+ *
12
+ * // 将函数标记为一个用户动作
13
+ * const handleSelect = userAction((date: Dater) => {
14
+ * currentDate.value = date
15
+ * emit('update:modelValue', date.format(fmt))
16
+ * })
17
+ *
18
+ * // modelValue 回显:用户活动期间跳过
19
+ * watch(() => props.modelValue, (v) => {
20
+ * if (isUserActive()) return
21
+ * currentDate.value = v ? date(v) : undefined
22
+ * })
23
+ * ```
24
+ */
25
+ function useUserAction() {
26
+ let actionCount = 0;
27
+ const isUserActive = () => {
28
+ return actionCount > 0;
29
+ };
30
+ const userAction = (fn) => {
31
+ return async (...args) => {
32
+ actionCount++;
33
+ try {
34
+ await fn(...args);
35
+ } catch (error) {
36
+ console.error(error);
37
+ }
38
+ await nextTick();
39
+ actionCount--;
40
+ };
41
+ };
42
+ return {
43
+ isUserActive,
44
+ userAction
45
+ };
46
+ }
47
+ //#endregion
48
+ export { useUserAction };
49
+
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/use-user-action/index.ts"],"sourcesContent":["import { nextTick } from 'vue'\n\nexport type UserAction = <T extends (...args: any[]) => void | Promise<void>>(\n fn: T\n) => (...args: Parameters<T>) => Promise<void>\n\nexport interface UserActionResult {\n isUserActive: () => boolean\n userAction: UserAction\n}\n\n/**\n * 创建用户活动\n * @description\n * 解决\"组件内部 emit 更新 → 外部 props 回传 → 监听 props 的副作用再次触发\n * 内部状态变更\"的循环更新问题。典型搭配:\n *\n * ```ts\n * const { userAction, isUserActive } = useUserAction()\n *\n * // 将函数标记为一个用户动作\n * const handleSelect = userAction((date: Dater) => {\n * currentDate.value = date\n * emit('update:modelValue', date.format(fmt))\n * })\n *\n * // modelValue 回显:用户活动期间跳过\n * watch(() => props.modelValue, (v) => {\n * if (isUserActive()) return\n * currentDate.value = v ? date(v) : undefined\n * })\n * ```\n */\nexport function useUserAction(): UserActionResult {\n let actionCount = 0\n const isUserActive = () => {\n return actionCount > 0\n }\n\n const userAction: UserAction = (fn) => {\n return async (...args) => {\n actionCount++\n\n try {\n await fn(...args)\n } catch (error) {\n console.error(error)\n }\n await nextTick()\n\n actionCount--\n }\n }\n\n return { isUserActive, userAction }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,gBAAkC;CAChD,IAAI,cAAc;CAClB,MAAM,qBAAqB;AACzB,SAAO,cAAc;;CAGvB,MAAM,cAA0B,OAAO;AACrC,SAAO,OAAO,GAAG,SAAS;AACxB;AAEA,OAAI;AACF,UAAM,GAAG,GAAG,KAAK;YACV,OAAO;AACd,YAAQ,MAAM,MAAM;;AAEtB,SAAM,UAAU;AAEhB;;;AAIJ,QAAO;EAAE;EAAc;EAAY"}
@@ -0,0 +1,69 @@
1
+ import { MaybeRefOrGetter, Ref, ShallowRef } from "vue";
2
+ import { EstimateSize, GetItemKey, VirtualAlign, VirtualItem, VirtualMeasurement, VirtualRange, VirtualScrollOptions, VirtualSnapshot, Virtualizer as Virtualizer$1, VirtualizerOptions } from "@cat-kit/fe";
3
+
4
+ //#region src/use-virtualizer/index.d.ts
5
+ type MaybeEl = HTMLElement | null | undefined;
6
+ interface UseVirtualizerOptions extends Omit<VirtualizerOptions, 'count'> {
7
+ /** 响应式虚拟项总数;内部 `watch` 驱动 `virtualizer.setCount`。 */
8
+ count: Ref<number>;
9
+ /**
10
+ * 响应式滚动容器;变更时自动 `connect` / `disconnect`。
11
+ * `immediate: true`,初始为 `null` 时等价于 `disconnect()`。
12
+ */
13
+ scrollEl: MaybeRefOrGetter<MaybeEl>;
14
+ /**
15
+ * 可选:内容承接元素。hook 订阅到结构性更新时直接写入
16
+ * `style.height = totalSize + 'px'`(水平模式为 `width`),
17
+ * **不经过 Vue 响应式**——因此 `totalSize` 的变化不会触发模板重渲染。
18
+ * 当引用切换为 `null` 时,会清空此前写入的内联尺寸,恢复 CSS 默认。
19
+ */
20
+ contentEl?: MaybeRefOrGetter<MaybeEl>;
21
+ /** 可选:首项前占位元素。语义同 `contentEl`,写 `beforeSize`。 */
22
+ beforeEl?: MaybeRefOrGetter<MaybeEl>;
23
+ /** 可选:末项后占位元素。语义同 `contentEl`,写 `afterSize`。 */
24
+ afterEl?: MaybeRefOrGetter<MaybeEl>;
25
+ }
26
+ interface UseVirtualizerReturned {
27
+ /**
28
+ * 底层 `Virtualizer` 实例。
29
+ *
30
+ * 未做包装;消费者可直接调用 `scrollToIndex` / `scrollToOffset` /
31
+ * `reset` / `measureMany` / `setOptions` / `measureElement` 等方法。
32
+ */
33
+ virtualizer: Virtualizer$1;
34
+ /**
35
+ * 完整快照 shallowRef。任一结构性变化(items/range/isScrolling/各种 size)都会更新。
36
+ * 大多数消费者只需 `items` / `isScrolling`,此 ref 仅在需要 `range` / `totalSize` 等
37
+ * 其它字段时使用。
38
+ */
39
+ snapshot: ShallowRef<VirtualSnapshot>;
40
+ /**
41
+ * 仅在底层 `items` 引用变化时才更新的 shallowRef。
42
+ *
43
+ * 独立于 `snapshot`,用于模板 `v-for`;这样 `isScrolling` 等字段的切换不会
44
+ * 连带触发 `items.map(...)` 的重新求值。
45
+ */
46
+ items: ShallowRef<VirtualItem[]>;
47
+ /** 仅在布尔值变化时更新的 `isScrolling` 状态。 */
48
+ isScrolling: ShallowRef<boolean>;
49
+ }
50
+ /**
51
+ * Vue 适配层:把 `@cat-kit/fe` 的 `Virtualizer` 生命周期与响应式系统对接。
52
+ *
53
+ * 设计原则:
54
+ * 1. **尺寸写入走 DOM**:`contentEl` / `beforeEl` / `afterEl` 一旦传入,
55
+ * hook 在 `subscribe` 回调中命令式写 `style.height`;消费者模板无需绑定
56
+ * `totalSize` / `beforeSize` / `afterSize`,滚动时 Vue 不会因尺寸变化重渲染。
57
+ * 2. **`items` / `isScrolling` 独立拆分**:底层 subscribe 一次回调可能同时包含
58
+ * items 变化与 isScrolling 切换;拆成两个 shallowRef 后,消费者 `v-for`
59
+ * 的 `computed(() => items.value.map(...))` 不会因 `isScrolling` 变化而重新执行。
60
+ * 3. **业务语义外置**:阈值判定 / key 组装 / `scrollToIndex` 对齐方式由消费者组装。
61
+ *
62
+ * 约束:
63
+ * - `initialOffset` / `initialViewport` 仅构造时生效,后续 `setOptions` 忽略(底层契约)。
64
+ * - 消费者若需运行时切换 `estimateSize` / `getItemKey` 等字段,调用 `virtualizer.setOptions(...)`。
65
+ */
66
+ declare function useVirtualizer(options: UseVirtualizerOptions): UseVirtualizerReturned;
67
+ //#endregion
68
+ export { type EstimateSize, type GetItemKey, UseVirtualizerOptions, UseVirtualizerReturned, type VirtualAlign, type VirtualItem, type VirtualMeasurement, type VirtualRange, type VirtualScrollOptions, type VirtualSnapshot, type Virtualizer$1 as Virtualizer, type VirtualizerOptions, useVirtualizer };
69
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { onScopeDispose, shallowRef, toValue, watch } from "vue";
2
+ import { Virtualizer } from "@cat-kit/fe";
3
+ //#region src/use-virtualizer/index.ts
4
+ function writeSize(el, size, horizontal) {
5
+ if (!el) return;
6
+ if (horizontal) el.style.width = `${size}px`;
7
+ else el.style.height = `${size}px`;
8
+ }
9
+ function clearSize(el, horizontal) {
10
+ if (!el) return;
11
+ if (horizontal) el.style.width = "";
12
+ else el.style.height = "";
13
+ }
14
+ /**
15
+ * Vue 适配层:把 `@cat-kit/fe` 的 `Virtualizer` 生命周期与响应式系统对接。
16
+ *
17
+ * 设计原则:
18
+ * 1. **尺寸写入走 DOM**:`contentEl` / `beforeEl` / `afterEl` 一旦传入,
19
+ * hook 在 `subscribe` 回调中命令式写 `style.height`;消费者模板无需绑定
20
+ * `totalSize` / `beforeSize` / `afterSize`,滚动时 Vue 不会因尺寸变化重渲染。
21
+ * 2. **`items` / `isScrolling` 独立拆分**:底层 subscribe 一次回调可能同时包含
22
+ * items 变化与 isScrolling 切换;拆成两个 shallowRef 后,消费者 `v-for`
23
+ * 的 `computed(() => items.value.map(...))` 不会因 `isScrolling` 变化而重新执行。
24
+ * 3. **业务语义外置**:阈值判定 / key 组装 / `scrollToIndex` 对齐方式由消费者组装。
25
+ *
26
+ * 约束:
27
+ * - `initialOffset` / `initialViewport` 仅构造时生效,后续 `setOptions` 忽略(底层契约)。
28
+ * - 消费者若需运行时切换 `estimateSize` / `getItemKey` 等字段,调用 `virtualizer.setOptions(...)`。
29
+ */
30
+ function useVirtualizer(options) {
31
+ const { count, scrollEl, contentEl, beforeEl, afterEl, ...initOptions } = options;
32
+ const horizontal = initOptions.horizontal === true;
33
+ const virtualizer = new Virtualizer({
34
+ ...initOptions,
35
+ count: count.value
36
+ });
37
+ const initialSnapshot = virtualizer.getSnapshot();
38
+ const snapshot = shallowRef(initialSnapshot);
39
+ const items = shallowRef(initialSnapshot.items);
40
+ const isScrolling = shallowRef(initialSnapshot.isScrolling);
41
+ function flushSizes(s) {
42
+ writeSize(toValue(contentEl) ?? null, s.totalSize, horizontal);
43
+ writeSize(toValue(beforeEl) ?? null, s.beforeSize, horizontal);
44
+ writeSize(toValue(afterEl) ?? null, s.afterSize, horizontal);
45
+ }
46
+ flushSizes(initialSnapshot);
47
+ const unsubscribe = virtualizer.subscribe((next) => {
48
+ snapshot.value = next;
49
+ if (next.items !== items.value) items.value = next.items;
50
+ if (next.isScrolling !== isScrolling.value) isScrolling.value = next.isScrolling;
51
+ flushSizes(next);
52
+ });
53
+ watch(count, (c) => {
54
+ virtualizer.setCount(c);
55
+ });
56
+ watch(() => toValue(scrollEl) ?? null, (el) => {
57
+ if (el) virtualizer.connect(el);
58
+ else virtualizer.disconnect();
59
+ }, { immediate: true });
60
+ function bindSizeSlot(source, pick) {
61
+ if (source === void 0) return;
62
+ watch(() => toValue(source) ?? null, (el, oldEl) => {
63
+ if (oldEl && oldEl !== el) clearSize(oldEl, horizontal);
64
+ if (el) writeSize(el, pick(snapshot.value), horizontal);
65
+ });
66
+ }
67
+ bindSizeSlot(contentEl, (s) => s.totalSize);
68
+ bindSizeSlot(beforeEl, (s) => s.beforeSize);
69
+ bindSizeSlot(afterEl, (s) => s.afterSize);
70
+ onScopeDispose(() => {
71
+ unsubscribe();
72
+ virtualizer.destroy();
73
+ clearSize(toValue(contentEl) ?? null, horizontal);
74
+ clearSize(toValue(beforeEl) ?? null, horizontal);
75
+ clearSize(toValue(afterEl) ?? null, horizontal);
76
+ });
77
+ return {
78
+ virtualizer,
79
+ snapshot,
80
+ items,
81
+ isScrolling
82
+ };
83
+ }
84
+ //#endregion
85
+ export { useVirtualizer };
86
+
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/use-virtualizer/index.ts"],"sourcesContent":["import {\n Virtualizer,\n type EstimateSize,\n type GetItemKey,\n type VirtualAlign,\n type VirtualItem,\n type VirtualMeasurement,\n type VirtualRange,\n type VirtualScrollOptions,\n type VirtualSnapshot,\n type VirtualizerOptions\n} from '@cat-kit/fe'\nimport {\n onScopeDispose,\n shallowRef,\n toValue,\n watch,\n type MaybeRefOrGetter,\n type Ref,\n type ShallowRef\n} from 'vue'\n\nexport type {\n EstimateSize,\n GetItemKey,\n VirtualAlign,\n VirtualItem,\n VirtualMeasurement,\n VirtualRange,\n VirtualScrollOptions,\n VirtualSnapshot,\n Virtualizer,\n VirtualizerOptions\n}\n\ntype MaybeEl = HTMLElement | null | undefined\n\nexport interface UseVirtualizerOptions extends Omit<VirtualizerOptions, 'count'> {\n /** 响应式虚拟项总数;内部 `watch` 驱动 `virtualizer.setCount`。 */\n count: Ref<number>\n /**\n * 响应式滚动容器;变更时自动 `connect` / `disconnect`。\n * `immediate: true`,初始为 `null` 时等价于 `disconnect()`。\n */\n scrollEl: MaybeRefOrGetter<MaybeEl>\n /**\n * 可选:内容承接元素。hook 订阅到结构性更新时直接写入\n * `style.height = totalSize + 'px'`(水平模式为 `width`),\n * **不经过 Vue 响应式**——因此 `totalSize` 的变化不会触发模板重渲染。\n * 当引用切换为 `null` 时,会清空此前写入的内联尺寸,恢复 CSS 默认。\n */\n contentEl?: MaybeRefOrGetter<MaybeEl>\n /** 可选:首项前占位元素。语义同 `contentEl`,写 `beforeSize`。 */\n beforeEl?: MaybeRefOrGetter<MaybeEl>\n /** 可选:末项后占位元素。语义同 `contentEl`,写 `afterSize`。 */\n afterEl?: MaybeRefOrGetter<MaybeEl>\n}\n\nexport interface UseVirtualizerReturned {\n /**\n * 底层 `Virtualizer` 实例。\n *\n * 未做包装;消费者可直接调用 `scrollToIndex` / `scrollToOffset` /\n * `reset` / `measureMany` / `setOptions` / `measureElement` 等方法。\n */\n virtualizer: Virtualizer\n /**\n * 完整快照 shallowRef。任一结构性变化(items/range/isScrolling/各种 size)都会更新。\n * 大多数消费者只需 `items` / `isScrolling`,此 ref 仅在需要 `range` / `totalSize` 等\n * 其它字段时使用。\n */\n snapshot: ShallowRef<VirtualSnapshot>\n /**\n * 仅在底层 `items` 引用变化时才更新的 shallowRef。\n *\n * 独立于 `snapshot`,用于模板 `v-for`;这样 `isScrolling` 等字段的切换不会\n * 连带触发 `items.map(...)` 的重新求值。\n */\n items: ShallowRef<VirtualItem[]>\n /** 仅在布尔值变化时更新的 `isScrolling` 状态。 */\n isScrolling: ShallowRef<boolean>\n}\n\nfunction writeSize(el: MaybeEl, size: number, horizontal: boolean): void {\n if (!el) return\n if (horizontal) el.style.width = `${size}px`\n else el.style.height = `${size}px`\n}\n\nfunction clearSize(el: MaybeEl, horizontal: boolean): void {\n if (!el) return\n if (horizontal) el.style.width = ''\n else el.style.height = ''\n}\n\n/**\n * Vue 适配层:把 `@cat-kit/fe` 的 `Virtualizer` 生命周期与响应式系统对接。\n *\n * 设计原则:\n * 1. **尺寸写入走 DOM**:`contentEl` / `beforeEl` / `afterEl` 一旦传入,\n * hook 在 `subscribe` 回调中命令式写 `style.height`;消费者模板无需绑定\n * `totalSize` / `beforeSize` / `afterSize`,滚动时 Vue 不会因尺寸变化重渲染。\n * 2. **`items` / `isScrolling` 独立拆分**:底层 subscribe 一次回调可能同时包含\n * items 变化与 isScrolling 切换;拆成两个 shallowRef 后,消费者 `v-for`\n * 的 `computed(() => items.value.map(...))` 不会因 `isScrolling` 变化而重新执行。\n * 3. **业务语义外置**:阈值判定 / key 组装 / `scrollToIndex` 对齐方式由消费者组装。\n *\n * 约束:\n * - `initialOffset` / `initialViewport` 仅构造时生效,后续 `setOptions` 忽略(底层契约)。\n * - 消费者若需运行时切换 `estimateSize` / `getItemKey` 等字段,调用 `virtualizer.setOptions(...)`。\n */\nexport function useVirtualizer(options: UseVirtualizerOptions): UseVirtualizerReturned {\n const { count, scrollEl, contentEl, beforeEl, afterEl, ...initOptions } = options\n const horizontal = initOptions.horizontal === true\n\n const virtualizer = new Virtualizer({ ...initOptions, count: count.value })\n\n const initialSnapshot = virtualizer.getSnapshot()\n const snapshot = shallowRef<VirtualSnapshot>(initialSnapshot)\n const items = shallowRef<VirtualItem[]>(initialSnapshot.items)\n const isScrolling = shallowRef<boolean>(initialSnapshot.isScrolling)\n\n function flushSizes(s: VirtualSnapshot): void {\n writeSize(toValue(contentEl) ?? null, s.totalSize, horizontal)\n writeSize(toValue(beforeEl) ?? null, s.beforeSize, horizontal)\n writeSize(toValue(afterEl) ?? null, s.afterSize, horizontal)\n }\n\n // 初次同步,覆盖构造阶段 count > 0 场景(容器未 connect 时 totalSize 已有值)。\n flushSizes(initialSnapshot)\n\n const unsubscribe = virtualizer.subscribe((next) => {\n snapshot.value = next\n if (next.items !== items.value) items.value = next.items\n if (next.isScrolling !== isScrolling.value) isScrolling.value = next.isScrolling\n flushSizes(next)\n })\n\n watch(count, (c) => {\n virtualizer.setCount(c)\n })\n\n watch(\n () => toValue(scrollEl) ?? null,\n (el) => {\n if (el) virtualizer.connect(el)\n else virtualizer.disconnect()\n },\n { immediate: true }\n )\n\n // 元素引用切换时:清旧元素的内联尺寸 + 立即给新元素写入当前快照值。\n // 不向 hook 返回值暴露这些元素(消费者用 :ref 绑定自己的 ref 即可)。\n function bindSizeSlot(\n source: MaybeRefOrGetter<MaybeEl> | undefined,\n pick: (s: VirtualSnapshot) => number\n ): void {\n if (source === undefined) return\n watch(\n () => toValue(source) ?? null,\n (el, oldEl) => {\n if (oldEl && oldEl !== el) clearSize(oldEl, horizontal)\n if (el) writeSize(el, pick(snapshot.value), horizontal)\n }\n )\n }\n\n bindSizeSlot(contentEl, (s) => s.totalSize)\n bindSizeSlot(beforeEl, (s) => s.beforeSize)\n bindSizeSlot(afterEl, (s) => s.afterSize)\n\n onScopeDispose(() => {\n unsubscribe()\n virtualizer.destroy()\n clearSize(toValue(contentEl) ?? null, horizontal)\n clearSize(toValue(beforeEl) ?? null, horizontal)\n clearSize(toValue(afterEl) ?? null, horizontal)\n })\n\n return { virtualizer, snapshot, items, isScrolling }\n}\n"],"mappings":";;;AAmFA,SAAS,UAAU,IAAa,MAAc,YAA2B;AACvE,KAAI,CAAC,GAAI;AACT,KAAI,WAAY,IAAG,MAAM,QAAQ,GAAG,KAAK;KACpC,IAAG,MAAM,SAAS,GAAG,KAAK;;AAGjC,SAAS,UAAU,IAAa,YAA2B;AACzD,KAAI,CAAC,GAAI;AACT,KAAI,WAAY,IAAG,MAAM,QAAQ;KAC5B,IAAG,MAAM,SAAS;;;;;;;;;;;;;;;;;;AAmBzB,SAAgB,eAAe,SAAwD;CACrF,MAAM,EAAE,OAAO,UAAU,WAAW,UAAU,SAAS,GAAG,gBAAgB;CAC1E,MAAM,aAAa,YAAY,eAAe;CAE9C,MAAM,cAAc,IAAI,YAAY;EAAE,GAAG;EAAa,OAAO,MAAM;EAAO,CAAC;CAE3E,MAAM,kBAAkB,YAAY,aAAa;CACjD,MAAM,WAAW,WAA4B,gBAAgB;CAC7D,MAAM,QAAQ,WAA0B,gBAAgB,MAAM;CAC9D,MAAM,cAAc,WAAoB,gBAAgB,YAAY;CAEpE,SAAS,WAAW,GAA0B;AAC5C,YAAU,QAAQ,UAAU,IAAI,MAAM,EAAE,WAAW,WAAW;AAC9D,YAAU,QAAQ,SAAS,IAAI,MAAM,EAAE,YAAY,WAAW;AAC9D,YAAU,QAAQ,QAAQ,IAAI,MAAM,EAAE,WAAW,WAAW;;AAI9D,YAAW,gBAAgB;CAE3B,MAAM,cAAc,YAAY,WAAW,SAAS;AAClD,WAAS,QAAQ;AACjB,MAAI,KAAK,UAAU,MAAM,MAAO,OAAM,QAAQ,KAAK;AACnD,MAAI,KAAK,gBAAgB,YAAY,MAAO,aAAY,QAAQ,KAAK;AACrE,aAAW,KAAK;GAChB;AAEF,OAAM,QAAQ,MAAM;AAClB,cAAY,SAAS,EAAE;GACvB;AAEF,aACQ,QAAQ,SAAS,IAAI,OAC1B,OAAO;AACN,MAAI,GAAI,aAAY,QAAQ,GAAG;MAC1B,aAAY,YAAY;IAE/B,EAAE,WAAW,MAAM,CACpB;CAID,SAAS,aACP,QACA,MACM;AACN,MAAI,WAAW,KAAA,EAAW;AAC1B,cACQ,QAAQ,OAAO,IAAI,OACxB,IAAI,UAAU;AACb,OAAI,SAAS,UAAU,GAAI,WAAU,OAAO,WAAW;AACvD,OAAI,GAAI,WAAU,IAAI,KAAK,SAAS,MAAM,EAAE,WAAW;IAE1D;;AAGH,cAAa,YAAY,MAAM,EAAE,UAAU;AAC3C,cAAa,WAAW,MAAM,EAAE,WAAW;AAC3C,cAAa,UAAU,MAAM,EAAE,UAAU;AAEzC,sBAAqB;AACnB,eAAa;AACb,cAAY,SAAS;AACrB,YAAU,QAAQ,UAAU,IAAI,MAAM,WAAW;AACjD,YAAU,QAAQ,SAAS,IAAI,MAAM,WAAW;AAChD,YAAU,QAAQ,QAAQ,IAAI,MAAM,WAAW;GAC/C;AAEF,QAAO;EAAE;EAAa;EAAU;EAAO;EAAa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/compositions",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -17,15 +17,12 @@
17
17
  "check-types": "tsc -p tsconfig.json --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "@cat-kit/core": "^1.0.1",
20
+ "@cat-kit/core": "^1.1.1",
21
+ "@cat-kit/fe": "^1.1.1",
21
22
  "@floating-ui/dom": "^1.7.6",
22
- "@tanstack/vue-virtual": "^3.13.23",
23
- "@veltra/utils": "1.0.8"
24
- },
25
- "devDependencies": {
26
- "tsdown": "^0.21.7"
23
+ "@veltra/utils": "1.0.9"
27
24
  },
28
25
  "peerDependencies": {
29
- "vue": "^3.5.0"
26
+ "vue": "^3.5.33"
30
27
  }
31
28
  }
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ export * from './use-focus'
10
10
 
11
11
  export * from './use-form-component'
12
12
 
13
- export * from './use-lock'
13
+ export * from './use-user-action'
14
14
 
15
15
  export * from './use-model'
16
16
 
@@ -22,4 +22,4 @@ export * from './use-resize-observer'
22
22
 
23
23
  export * from './use-transition'
24
24
 
25
- export * from './use-virtual'
25
+ export * from './use-virtualizer'
@@ -0,0 +1,56 @@
1
+ import { nextTick } from 'vue'
2
+
3
+ export type UserAction = <T extends (...args: any[]) => void | Promise<void>>(
4
+ fn: T
5
+ ) => (...args: Parameters<T>) => Promise<void>
6
+
7
+ export interface UserActionResult {
8
+ isUserActive: () => boolean
9
+ userAction: UserAction
10
+ }
11
+
12
+ /**
13
+ * 创建用户活动
14
+ * @description
15
+ * 解决"组件内部 emit 更新 → 外部 props 回传 → 监听 props 的副作用再次触发
16
+ * 内部状态变更"的循环更新问题。典型搭配:
17
+ *
18
+ * ```ts
19
+ * const { userAction, isUserActive } = useUserAction()
20
+ *
21
+ * // 将函数标记为一个用户动作
22
+ * const handleSelect = userAction((date: Dater) => {
23
+ * currentDate.value = date
24
+ * emit('update:modelValue', date.format(fmt))
25
+ * })
26
+ *
27
+ * // modelValue 回显:用户活动期间跳过
28
+ * watch(() => props.modelValue, (v) => {
29
+ * if (isUserActive()) return
30
+ * currentDate.value = v ? date(v) : undefined
31
+ * })
32
+ * ```
33
+ */
34
+ export function useUserAction(): UserActionResult {
35
+ let actionCount = 0
36
+ const isUserActive = () => {
37
+ return actionCount > 0
38
+ }
39
+
40
+ const userAction: UserAction = (fn) => {
41
+ return async (...args) => {
42
+ actionCount++
43
+
44
+ try {
45
+ await fn(...args)
46
+ } catch (error) {
47
+ console.error(error)
48
+ }
49
+ await nextTick()
50
+
51
+ actionCount--
52
+ }
53
+ }
54
+
55
+ return { isUserActive, userAction }
56
+ }
@@ -0,0 +1,181 @@
1
+ import {
2
+ Virtualizer,
3
+ type EstimateSize,
4
+ type GetItemKey,
5
+ type VirtualAlign,
6
+ type VirtualItem,
7
+ type VirtualMeasurement,
8
+ type VirtualRange,
9
+ type VirtualScrollOptions,
10
+ type VirtualSnapshot,
11
+ type VirtualizerOptions
12
+ } from '@cat-kit/fe'
13
+ import {
14
+ onScopeDispose,
15
+ shallowRef,
16
+ toValue,
17
+ watch,
18
+ type MaybeRefOrGetter,
19
+ type Ref,
20
+ type ShallowRef
21
+ } from 'vue'
22
+
23
+ export type {
24
+ EstimateSize,
25
+ GetItemKey,
26
+ VirtualAlign,
27
+ VirtualItem,
28
+ VirtualMeasurement,
29
+ VirtualRange,
30
+ VirtualScrollOptions,
31
+ VirtualSnapshot,
32
+ Virtualizer,
33
+ VirtualizerOptions
34
+ }
35
+
36
+ type MaybeEl = HTMLElement | null | undefined
37
+
38
+ export interface UseVirtualizerOptions extends Omit<VirtualizerOptions, 'count'> {
39
+ /** 响应式虚拟项总数;内部 `watch` 驱动 `virtualizer.setCount`。 */
40
+ count: Ref<number>
41
+ /**
42
+ * 响应式滚动容器;变更时自动 `connect` / `disconnect`。
43
+ * `immediate: true`,初始为 `null` 时等价于 `disconnect()`。
44
+ */
45
+ scrollEl: MaybeRefOrGetter<MaybeEl>
46
+ /**
47
+ * 可选:内容承接元素。hook 订阅到结构性更新时直接写入
48
+ * `style.height = totalSize + 'px'`(水平模式为 `width`),
49
+ * **不经过 Vue 响应式**——因此 `totalSize` 的变化不会触发模板重渲染。
50
+ * 当引用切换为 `null` 时,会清空此前写入的内联尺寸,恢复 CSS 默认。
51
+ */
52
+ contentEl?: MaybeRefOrGetter<MaybeEl>
53
+ /** 可选:首项前占位元素。语义同 `contentEl`,写 `beforeSize`。 */
54
+ beforeEl?: MaybeRefOrGetter<MaybeEl>
55
+ /** 可选:末项后占位元素。语义同 `contentEl`,写 `afterSize`。 */
56
+ afterEl?: MaybeRefOrGetter<MaybeEl>
57
+ }
58
+
59
+ export interface UseVirtualizerReturned {
60
+ /**
61
+ * 底层 `Virtualizer` 实例。
62
+ *
63
+ * 未做包装;消费者可直接调用 `scrollToIndex` / `scrollToOffset` /
64
+ * `reset` / `measureMany` / `setOptions` / `measureElement` 等方法。
65
+ */
66
+ virtualizer: Virtualizer
67
+ /**
68
+ * 完整快照 shallowRef。任一结构性变化(items/range/isScrolling/各种 size)都会更新。
69
+ * 大多数消费者只需 `items` / `isScrolling`,此 ref 仅在需要 `range` / `totalSize` 等
70
+ * 其它字段时使用。
71
+ */
72
+ snapshot: ShallowRef<VirtualSnapshot>
73
+ /**
74
+ * 仅在底层 `items` 引用变化时才更新的 shallowRef。
75
+ *
76
+ * 独立于 `snapshot`,用于模板 `v-for`;这样 `isScrolling` 等字段的切换不会
77
+ * 连带触发 `items.map(...)` 的重新求值。
78
+ */
79
+ items: ShallowRef<VirtualItem[]>
80
+ /** 仅在布尔值变化时更新的 `isScrolling` 状态。 */
81
+ isScrolling: ShallowRef<boolean>
82
+ }
83
+
84
+ function writeSize(el: MaybeEl, size: number, horizontal: boolean): void {
85
+ if (!el) return
86
+ if (horizontal) el.style.width = `${size}px`
87
+ else el.style.height = `${size}px`
88
+ }
89
+
90
+ function clearSize(el: MaybeEl, horizontal: boolean): void {
91
+ if (!el) return
92
+ if (horizontal) el.style.width = ''
93
+ else el.style.height = ''
94
+ }
95
+
96
+ /**
97
+ * Vue 适配层:把 `@cat-kit/fe` 的 `Virtualizer` 生命周期与响应式系统对接。
98
+ *
99
+ * 设计原则:
100
+ * 1. **尺寸写入走 DOM**:`contentEl` / `beforeEl` / `afterEl` 一旦传入,
101
+ * hook 在 `subscribe` 回调中命令式写 `style.height`;消费者模板无需绑定
102
+ * `totalSize` / `beforeSize` / `afterSize`,滚动时 Vue 不会因尺寸变化重渲染。
103
+ * 2. **`items` / `isScrolling` 独立拆分**:底层 subscribe 一次回调可能同时包含
104
+ * items 变化与 isScrolling 切换;拆成两个 shallowRef 后,消费者 `v-for`
105
+ * 的 `computed(() => items.value.map(...))` 不会因 `isScrolling` 变化而重新执行。
106
+ * 3. **业务语义外置**:阈值判定 / key 组装 / `scrollToIndex` 对齐方式由消费者组装。
107
+ *
108
+ * 约束:
109
+ * - `initialOffset` / `initialViewport` 仅构造时生效,后续 `setOptions` 忽略(底层契约)。
110
+ * - 消费者若需运行时切换 `estimateSize` / `getItemKey` 等字段,调用 `virtualizer.setOptions(...)`。
111
+ */
112
+ export function useVirtualizer(options: UseVirtualizerOptions): UseVirtualizerReturned {
113
+ const { count, scrollEl, contentEl, beforeEl, afterEl, ...initOptions } = options
114
+ const horizontal = initOptions.horizontal === true
115
+
116
+ const virtualizer = new Virtualizer({ ...initOptions, count: count.value })
117
+
118
+ const initialSnapshot = virtualizer.getSnapshot()
119
+ const snapshot = shallowRef<VirtualSnapshot>(initialSnapshot)
120
+ const items = shallowRef<VirtualItem[]>(initialSnapshot.items)
121
+ const isScrolling = shallowRef<boolean>(initialSnapshot.isScrolling)
122
+
123
+ function flushSizes(s: VirtualSnapshot): void {
124
+ writeSize(toValue(contentEl) ?? null, s.totalSize, horizontal)
125
+ writeSize(toValue(beforeEl) ?? null, s.beforeSize, horizontal)
126
+ writeSize(toValue(afterEl) ?? null, s.afterSize, horizontal)
127
+ }
128
+
129
+ // 初次同步,覆盖构造阶段 count > 0 场景(容器未 connect 时 totalSize 已有值)。
130
+ flushSizes(initialSnapshot)
131
+
132
+ const unsubscribe = virtualizer.subscribe((next) => {
133
+ snapshot.value = next
134
+ if (next.items !== items.value) items.value = next.items
135
+ if (next.isScrolling !== isScrolling.value) isScrolling.value = next.isScrolling
136
+ flushSizes(next)
137
+ })
138
+
139
+ watch(count, (c) => {
140
+ virtualizer.setCount(c)
141
+ })
142
+
143
+ watch(
144
+ () => toValue(scrollEl) ?? null,
145
+ (el) => {
146
+ if (el) virtualizer.connect(el)
147
+ else virtualizer.disconnect()
148
+ },
149
+ { immediate: true }
150
+ )
151
+
152
+ // 元素引用切换时:清旧元素的内联尺寸 + 立即给新元素写入当前快照值。
153
+ // 不向 hook 返回值暴露这些元素(消费者用 :ref 绑定自己的 ref 即可)。
154
+ function bindSizeSlot(
155
+ source: MaybeRefOrGetter<MaybeEl> | undefined,
156
+ pick: (s: VirtualSnapshot) => number
157
+ ): void {
158
+ if (source === undefined) return
159
+ watch(
160
+ () => toValue(source) ?? null,
161
+ (el, oldEl) => {
162
+ if (oldEl && oldEl !== el) clearSize(oldEl, horizontal)
163
+ if (el) writeSize(el, pick(snapshot.value), horizontal)
164
+ }
165
+ )
166
+ }
167
+
168
+ bindSizeSlot(contentEl, (s) => s.totalSize)
169
+ bindSizeSlot(beforeEl, (s) => s.beforeSize)
170
+ bindSizeSlot(afterEl, (s) => s.afterSize)
171
+
172
+ onScopeDispose(() => {
173
+ unsubscribe()
174
+ virtualizer.destroy()
175
+ clearSize(toValue(contentEl) ?? null, horizontal)
176
+ clearSize(toValue(beforeEl) ?? null, horizontal)
177
+ clearSize(toValue(afterEl) ?? null, horizontal)
178
+ })
179
+
180
+ return { virtualizer, snapshot, items, isScrolling }
181
+ }
@@ -1,28 +0,0 @@
1
- //#region src/use-lock/index.d.ts
2
- type Update = (fn: Function) => any;
3
- type Lock = (fn: Function) => Promise<void>;
4
- interface Updater {
5
- /**
6
- * 更新
7
- * @description 在非锁定时执行传入的函数
8
- */
9
- update: Update;
10
- /**
11
- * 更新并锁定
12
- * @description 执行传入的函数,并锁定更新操作,直到函数执行完成
13
- */
14
- updateAndLock: Lock;
15
- }
16
- /**
17
- * 数据更新锁
18
- * @description
19
- * 更新锁主要用于防止组件数据更新时,循环触发更新。
20
- *
21
- * @returns 该函数返回两个函数:
22
- * 1. update: 更新函数,在非锁定时执行传入的函数
23
- * 2. lock: 锁定函数,执行时会锁定更新操作,直到锁定操作结束
24
- */
25
- declare function useUpdateLock(): Updater;
26
- //#endregion
27
- export { Updater, useUpdateLock };
28
- //# sourceMappingURL=index.d.ts.map
@@ -1,36 +0,0 @@
1
- import { nextTick } from "vue";
2
- //#region src/use-lock/index.ts
3
- /**
4
- * 数据更新锁
5
- * @description
6
- * 更新锁主要用于防止组件数据更新时,循环触发更新。
7
- *
8
- * @returns 该函数返回两个函数:
9
- * 1. update: 更新函数,在非锁定时执行传入的函数
10
- * 2. lock: 锁定函数,执行时会锁定更新操作,直到锁定操作结束
11
- */
12
- function useUpdateLock() {
13
- let lockedCount = 0;
14
- function update(fn) {
15
- if (lockedCount > 0) return;
16
- return fn();
17
- }
18
- async function updateAndLock(fn) {
19
- lockedCount++;
20
- try {
21
- await fn();
22
- } catch (error) {
23
- console.error(error);
24
- }
25
- await nextTick();
26
- lockedCount--;
27
- }
28
- return {
29
- updateAndLock,
30
- update
31
- };
32
- }
33
- //#endregion
34
- export { useUpdateLock };
35
-
36
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/use-lock/index.ts"],"sourcesContent":["import { nextTick } from 'vue'\n\ntype Update = (fn: Function) => any\ntype Lock = (fn: Function) => Promise<void>\n\nexport interface Updater {\n /**\n * 更新\n * @description 在非锁定时执行传入的函数\n */\n update: Update\n /**\n * 更新并锁定\n * @description 执行传入的函数,并锁定更新操作,直到函数执行完成\n */\n updateAndLock: Lock\n}\n\n/**\n * 数据更新锁\n * @description\n * 更新锁主要用于防止组件数据更新时,循环触发更新。\n *\n * @returns 该函数返回两个函数:\n * 1. update: 更新函数,在非锁定时执行传入的函数\n * 2. lock: 锁定函数,执行时会锁定更新操作,直到锁定操作结束\n */\nexport function useUpdateLock(): Updater {\n let lockedCount = 0\n\n function update(fn: Function) {\n if (lockedCount > 0) return\n return fn()\n }\n\n async function updateAndLock(fn: Function) {\n lockedCount++\n\n try {\n await fn()\n } catch (error) {\n console.error(error)\n }\n await nextTick()\n\n lockedCount--\n }\n\n return { updateAndLock, update }\n}\n"],"mappings":";;;;;;;;;;;AA2BA,SAAgB,gBAAyB;CACvC,IAAI,cAAc;CAElB,SAAS,OAAO,IAAc;AAC5B,MAAI,cAAc,EAAG;AACrB,SAAO,IAAI;;CAGb,eAAe,cAAc,IAAc;AACzC;AAEA,MAAI;AACF,SAAM,IAAI;WACH,OAAO;AACd,WAAQ,MAAM,MAAM;;AAEtB,QAAM,UAAU;AAEhB;;AAGF,QAAO;EAAE;EAAe;EAAQ"}
@@ -1,30 +0,0 @@
1
- import { ComputedRef, Ref, ShallowRef } from "vue";
2
- import { VirtualItem } from "@tanstack/vue-virtual";
3
-
4
- //#region src/use-virtual/index.d.ts
5
- interface Options {
6
- /** 指定启用虚拟列表的阈值 */
7
- virtualThreshold?: number | Ref<number | undefined>;
8
- /** 数量 */
9
- count: Ref<number>;
10
- /** 滚动容器 */
11
- scrollEl: ShallowRef<HTMLElement | null>;
12
- /** 估算高度(宽度) */
13
- estimateSize?: (index: number) => number;
14
- /** 列表项之间的间距 */
15
- gap?: number;
16
- }
17
- type CustomVirtualItem = Omit<VirtualItem, 'key'> & {
18
- key: number | string;
19
- };
20
- type VirtualReturned = {
21
- /** 虚拟列表 */virtualList: ShallowRef<CustomVirtualItem[]>; /** 总高度 */
22
- totalHeight: ShallowRef<number>; /** 测量元素高度 */
23
- measureElement: (el: any) => void; /** 滚动到指定索引 */
24
- scrollTo: (index: number) => void; /** 是否启用虚拟列表 */
25
- virtualEnabled: ComputedRef<boolean>;
26
- };
27
- declare function useVirtual(options: Options): VirtualReturned;
28
- //#endregion
29
- export { VirtualReturned, useVirtual };
30
- //# sourceMappingURL=index.d.ts.map
@@ -1,67 +0,0 @@
1
- import { computed, isRef, onScopeDispose, shallowRef, watch } from "vue";
2
- import { Virtualizer, elementScroll, observeElementOffset, observeElementRect } from "@tanstack/vue-virtual";
3
- //#region src/use-virtual/index.ts
4
- const defaultEstimateSize = () => 34;
5
- function useVirtual(options) {
6
- const { count, scrollEl, estimateSize, virtualThreshold, gap } = options;
7
- const enabled = computed(() => {
8
- if (isRef(virtualThreshold)) return virtualThreshold.value ? count.value > virtualThreshold.value : true;
9
- return virtualThreshold ? count.value > virtualThreshold : true;
10
- });
11
- const virtualList = shallowRef([]);
12
- /** 总高度 */
13
- const totalHeight = shallowRef(0);
14
- function updateVirtualList() {
15
- if (enabled.value) {
16
- totalHeight.value = v.getTotalSize();
17
- virtualList.value = v.getVirtualItems();
18
- }
19
- }
20
- const virtualizerOptions = computed(() => {
21
- return {
22
- enabled: enabled.value,
23
- count: count.value,
24
- getScrollElement: () => scrollEl.value,
25
- estimateSize: estimateSize ?? defaultEstimateSize,
26
- overscan: 3,
27
- gap,
28
- observeElementRect,
29
- observeElementOffset,
30
- scrollToFn: elementScroll,
31
- onChange: updateVirtualList
32
- };
33
- });
34
- const v = new Virtualizer(virtualizerOptions.value);
35
- updateVirtualList();
36
- const cleanup = v._didMount();
37
- watch(scrollEl, (el) => {
38
- el && v._willUpdate();
39
- }, { immediate: true });
40
- watch(() => virtualizerOptions.value, (o) => {
41
- v.setOptions(o);
42
- v._willUpdate();
43
- updateVirtualList();
44
- });
45
- onScopeDispose(() => {
46
- cleanup();
47
- });
48
- function scrollTo(index) {
49
- v.scrollToIndex(index, { align: "center" });
50
- }
51
- /** 测量元素高度 */
52
- function measureElement(el) {
53
- if (!el) return;
54
- v.measureElement(el);
55
- }
56
- return {
57
- virtualEnabled: enabled,
58
- virtualList,
59
- totalHeight,
60
- measureElement,
61
- scrollTo
62
- };
63
- }
64
- //#endregion
65
- export { useVirtual };
66
-
67
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/use-virtual/index.ts"],"sourcesContent":["import {\n elementScroll,\n observeElementOffset,\n observeElementRect,\n type VirtualItem,\n Virtualizer\n} from '@tanstack/vue-virtual'\nimport {\n computed,\n isRef,\n onScopeDispose,\n shallowRef,\n watch,\n type ComputedRef,\n type Ref,\n type ShallowRef\n} from 'vue'\n\ninterface Options {\n /** 指定启用虚拟列表的阈值 */\n virtualThreshold?: number | Ref<number | undefined>\n /** 数量 */\n count: Ref<number>\n /** 滚动容器 */\n scrollEl: ShallowRef<HTMLElement | null>\n /** 估算高度(宽度) */\n estimateSize?: (index: number) => number\n /** 列表项之间的间距 */\n gap?: number\n}\n\ntype CustomVirtualItem = Omit<VirtualItem, 'key'> & { key: number | string }\n\nexport type VirtualReturned = {\n /** 虚拟列表 */\n virtualList: ShallowRef<CustomVirtualItem[]>\n /** 总高度 */\n totalHeight: ShallowRef<number>\n /** 测量元素高度 */\n measureElement: (el: any) => void\n /** 滚动到指定索引 */\n scrollTo: (index: number) => void\n /** 是否启用虚拟列表 */\n virtualEnabled: ComputedRef<boolean>\n}\n\nconst defaultEstimateSize = () => 34\n\nexport function useVirtual(options: Options): VirtualReturned {\n const { count, scrollEl, estimateSize, virtualThreshold, gap } = options\n\n const enabled = computed(() => {\n if (isRef(virtualThreshold)) {\n return virtualThreshold.value ? count.value > virtualThreshold.value : true\n }\n\n return virtualThreshold ? count.value > virtualThreshold : true\n })\n\n const virtualList = shallowRef<CustomVirtualItem[]>([])\n\n /** 总高度 */\n const totalHeight = shallowRef(0)\n\n function updateVirtualList() {\n if (enabled.value) {\n totalHeight.value = v.getTotalSize()\n virtualList.value = v.getVirtualItems() as CustomVirtualItem[]\n }\n }\n\n const virtualizerOptions = computed(() => {\n return {\n enabled: enabled.value,\n count: count.value,\n getScrollElement: () => scrollEl.value,\n estimateSize: estimateSize ?? defaultEstimateSize,\n overscan: 3,\n gap,\n observeElementRect: observeElementRect,\n observeElementOffset: observeElementOffset,\n scrollToFn: elementScroll,\n onChange: updateVirtualList\n }\n })\n\n const v = new Virtualizer(virtualizerOptions.value)\n\n updateVirtualList()\n\n const cleanup = v._didMount()\n\n watch(\n scrollEl,\n (el) => {\n el && v._willUpdate()\n },\n { immediate: true }\n )\n\n watch(\n () => virtualizerOptions.value,\n (o) => {\n v.setOptions(o)\n\n v._willUpdate()\n\n updateVirtualList()\n }\n )\n\n onScopeDispose(() => {\n cleanup()\n })\n\n function scrollTo(index: number) {\n v.scrollToIndex(index, { align: 'center' })\n }\n\n /** 测量元素高度 */\n function measureElement(el: Element) {\n if (!el) return\n\n v.measureElement(el)\n\n return undefined\n }\n\n return { virtualEnabled: enabled, virtualList, totalHeight, measureElement, scrollTo }\n}\n"],"mappings":";;;AA8CA,MAAM,4BAA4B;AAElC,SAAgB,WAAW,SAAmC;CAC5D,MAAM,EAAE,OAAO,UAAU,cAAc,kBAAkB,QAAQ;CAEjE,MAAM,UAAU,eAAe;AAC7B,MAAI,MAAM,iBAAiB,CACzB,QAAO,iBAAiB,QAAQ,MAAM,QAAQ,iBAAiB,QAAQ;AAGzE,SAAO,mBAAmB,MAAM,QAAQ,mBAAmB;GAC3D;CAEF,MAAM,cAAc,WAAgC,EAAE,CAAC;;CAGvD,MAAM,cAAc,WAAW,EAAE;CAEjC,SAAS,oBAAoB;AAC3B,MAAI,QAAQ,OAAO;AACjB,eAAY,QAAQ,EAAE,cAAc;AACpC,eAAY,QAAQ,EAAE,iBAAiB;;;CAI3C,MAAM,qBAAqB,eAAe;AACxC,SAAO;GACL,SAAS,QAAQ;GACjB,OAAO,MAAM;GACb,wBAAwB,SAAS;GACjC,cAAc,gBAAgB;GAC9B,UAAU;GACV;GACoB;GACE;GACtB,YAAY;GACZ,UAAU;GACX;GACD;CAEF,MAAM,IAAI,IAAI,YAAY,mBAAmB,MAAM;AAEnD,oBAAmB;CAEnB,MAAM,UAAU,EAAE,WAAW;AAE7B,OACE,WACC,OAAO;AACN,QAAM,EAAE,aAAa;IAEvB,EAAE,WAAW,MAAM,CACpB;AAED,aACQ,mBAAmB,QACxB,MAAM;AACL,IAAE,WAAW,EAAE;AAEf,IAAE,aAAa;AAEf,qBAAmB;GAEtB;AAED,sBAAqB;AACnB,WAAS;GACT;CAEF,SAAS,SAAS,OAAe;AAC/B,IAAE,cAAc,OAAO,EAAE,OAAO,UAAU,CAAC;;;CAI7C,SAAS,eAAe,IAAa;AACnC,MAAI,CAAC,GAAI;AAET,IAAE,eAAe,GAAG;;AAKtB,QAAO;EAAE,gBAAgB;EAAS;EAAa;EAAa;EAAgB;EAAU"}
@@ -1,50 +0,0 @@
1
- import { nextTick } from 'vue'
2
-
3
- type Update = (fn: Function) => any
4
- type Lock = (fn: Function) => Promise<void>
5
-
6
- export interface Updater {
7
- /**
8
- * 更新
9
- * @description 在非锁定时执行传入的函数
10
- */
11
- update: Update
12
- /**
13
- * 更新并锁定
14
- * @description 执行传入的函数,并锁定更新操作,直到函数执行完成
15
- */
16
- updateAndLock: Lock
17
- }
18
-
19
- /**
20
- * 数据更新锁
21
- * @description
22
- * 更新锁主要用于防止组件数据更新时,循环触发更新。
23
- *
24
- * @returns 该函数返回两个函数:
25
- * 1. update: 更新函数,在非锁定时执行传入的函数
26
- * 2. lock: 锁定函数,执行时会锁定更新操作,直到锁定操作结束
27
- */
28
- export function useUpdateLock(): Updater {
29
- let lockedCount = 0
30
-
31
- function update(fn: Function) {
32
- if (lockedCount > 0) return
33
- return fn()
34
- }
35
-
36
- async function updateAndLock(fn: Function) {
37
- lockedCount++
38
-
39
- try {
40
- await fn()
41
- } catch (error) {
42
- console.error(error)
43
- }
44
- await nextTick()
45
-
46
- lockedCount--
47
- }
48
-
49
- return { updateAndLock, update }
50
- }
@@ -1,130 +0,0 @@
1
- import {
2
- elementScroll,
3
- observeElementOffset,
4
- observeElementRect,
5
- type VirtualItem,
6
- Virtualizer
7
- } from '@tanstack/vue-virtual'
8
- import {
9
- computed,
10
- isRef,
11
- onScopeDispose,
12
- shallowRef,
13
- watch,
14
- type ComputedRef,
15
- type Ref,
16
- type ShallowRef
17
- } from 'vue'
18
-
19
- interface Options {
20
- /** 指定启用虚拟列表的阈值 */
21
- virtualThreshold?: number | Ref<number | undefined>
22
- /** 数量 */
23
- count: Ref<number>
24
- /** 滚动容器 */
25
- scrollEl: ShallowRef<HTMLElement | null>
26
- /** 估算高度(宽度) */
27
- estimateSize?: (index: number) => number
28
- /** 列表项之间的间距 */
29
- gap?: number
30
- }
31
-
32
- type CustomVirtualItem = Omit<VirtualItem, 'key'> & { key: number | string }
33
-
34
- export type VirtualReturned = {
35
- /** 虚拟列表 */
36
- virtualList: ShallowRef<CustomVirtualItem[]>
37
- /** 总高度 */
38
- totalHeight: ShallowRef<number>
39
- /** 测量元素高度 */
40
- measureElement: (el: any) => void
41
- /** 滚动到指定索引 */
42
- scrollTo: (index: number) => void
43
- /** 是否启用虚拟列表 */
44
- virtualEnabled: ComputedRef<boolean>
45
- }
46
-
47
- const defaultEstimateSize = () => 34
48
-
49
- export function useVirtual(options: Options): VirtualReturned {
50
- const { count, scrollEl, estimateSize, virtualThreshold, gap } = options
51
-
52
- const enabled = computed(() => {
53
- if (isRef(virtualThreshold)) {
54
- return virtualThreshold.value ? count.value > virtualThreshold.value : true
55
- }
56
-
57
- return virtualThreshold ? count.value > virtualThreshold : true
58
- })
59
-
60
- const virtualList = shallowRef<CustomVirtualItem[]>([])
61
-
62
- /** 总高度 */
63
- const totalHeight = shallowRef(0)
64
-
65
- function updateVirtualList() {
66
- if (enabled.value) {
67
- totalHeight.value = v.getTotalSize()
68
- virtualList.value = v.getVirtualItems() as CustomVirtualItem[]
69
- }
70
- }
71
-
72
- const virtualizerOptions = computed(() => {
73
- return {
74
- enabled: enabled.value,
75
- count: count.value,
76
- getScrollElement: () => scrollEl.value,
77
- estimateSize: estimateSize ?? defaultEstimateSize,
78
- overscan: 3,
79
- gap,
80
- observeElementRect: observeElementRect,
81
- observeElementOffset: observeElementOffset,
82
- scrollToFn: elementScroll,
83
- onChange: updateVirtualList
84
- }
85
- })
86
-
87
- const v = new Virtualizer(virtualizerOptions.value)
88
-
89
- updateVirtualList()
90
-
91
- const cleanup = v._didMount()
92
-
93
- watch(
94
- scrollEl,
95
- (el) => {
96
- el && v._willUpdate()
97
- },
98
- { immediate: true }
99
- )
100
-
101
- watch(
102
- () => virtualizerOptions.value,
103
- (o) => {
104
- v.setOptions(o)
105
-
106
- v._willUpdate()
107
-
108
- updateVirtualList()
109
- }
110
- )
111
-
112
- onScopeDispose(() => {
113
- cleanup()
114
- })
115
-
116
- function scrollTo(index: number) {
117
- v.scrollToIndex(index, { align: 'center' })
118
- }
119
-
120
- /** 测量元素高度 */
121
- function measureElement(el: Element) {
122
- if (!el) return
123
-
124
- v.measureElement(el)
125
-
126
- return undefined
127
- }
128
-
129
- return { virtualEnabled: enabled, virtualList, totalHeight, measureElement, scrollTo }
130
- }