vue-stack-tabs 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ /**
2
+ * 自定义滚动容器适配器契约。
3
+ *
4
+ * 宿主通过 `<VueStackTabs :scroll-container="...">` 传入自己的滚动组件后,
5
+ * 库通过组件 `expose` 的对象读取下面这组能力,把滚动位置记忆与上下阴影
6
+ * 作用到真实的滚动元素上:
7
+ *
8
+ * - 只暴露 `scrollElement`:适用于所有「原生 scrollTop 驱动、只替换滚动条外观」
9
+ * 的组件(Element Plus `ElScrollbar`、OverlayScrollbars、SimpleBar 等);
10
+ * - 额外实现 `getPosition` / `setPosition` / `getMetrics` / `onScroll`:
11
+ * 适用于 BetterScroll、虚拟滚动等不产生原生滚动的实现。
12
+ *
13
+ * 所有可选项缺失时都会回退到原生 `scrollElement` 的相应属性,再缺失则
14
+ * 视为该能力不可用(例如关闭阴影),不会抛错。
15
+ */
16
+ export interface StackTabsScrollPosition {
17
+ top: number
18
+ left: number
19
+ }
20
+
21
+ export interface StackTabsScrollMetrics {
22
+ scrollTop: number
23
+ scrollHeight: number
24
+ clientHeight: number
25
+ }
26
+
27
+ export interface StackTabsScrollAdapter {
28
+ /**
29
+ * 真实滚动元素。原生滚动型组件只需暴露这一个字段。
30
+ * 命令式滚动组件(无原生滚动)可以不提供,改由下面可选方法承载。
31
+ */
32
+ scrollElement?: HTMLElement | null
33
+ /** 读取滚动位置;缺失时读取 `scrollElement.scrollTop / scrollLeft`。 */
34
+ getPosition?: () => StackTabsScrollPosition | null | undefined
35
+ /** 写入滚动位置;缺失时写入 `scrollElement.scrollTop / scrollLeft`。 */
36
+ setPosition?: (position: StackTabsScrollPosition) => void
37
+ /** 阴影计算所需的滚动尺寸;缺失时读取 `scrollElement` 的原生属性。 */
38
+ getMetrics?: () => StackTabsScrollMetrics | null | undefined
39
+ /** 订阅滚动;返回取消订阅函数。缺失时监听 `scrollElement` 的 scroll 事件。 */
40
+ onScroll?: (listener: () => void) => () => void
41
+ }
42
+
43
+ /**
44
+ * 窄类型:只实现原生滚动元素暴露的适配器。
45
+ * 宿主可以只实现它,不必被可选方法干扰。
46
+ */
47
+ export type StackTabsScrollElementInstance = Pick<StackTabsScrollAdapter, 'scrollElement'>
48
+
49
+ const isScrollElement = (value: unknown): value is HTMLElement =>
50
+ typeof HTMLElement !== 'undefined' && value instanceof HTMLElement
51
+
52
+ /** 解包 ref / getter 形式的 `scrollElement`,兼容 `defineExpose({ scrollElement: computed(...) })`。 */
53
+ const unwrapScrollElement = (value: unknown): HTMLElement | null => {
54
+ if (!value) return null
55
+ if (isScrollElement(value)) return value
56
+ if (typeof value === 'function') return unwrapScrollElement((value as () => unknown)())
57
+ const nested = (value as { value?: unknown }).value
58
+ return isScrollElement(nested) ? nested : null
59
+ }
60
+
61
+ const toFiniteNumber = (value: unknown): number =>
62
+ typeof value === 'number' && Number.isFinite(value) ? value : 0
63
+
64
+ const normalizePosition = (
65
+ position: StackTabsScrollPosition | null | undefined
66
+ ): StackTabsScrollPosition | null => {
67
+ if (!position) return null
68
+ return { top: toFiniteNumber(position.top), left: toFiniteNumber(position.left) }
69
+ }
70
+
71
+ /**
72
+ * 把自定义滚动组件的 `expose` 实例归一化为适配器。
73
+ *
74
+ * - 直接传 HTMLElement 时把它当作 `scrollElement`;
75
+ * - 实例上的方法以实例为 this 调用;
76
+ * - 完全没有可用能力时返回 null,调用方按「没有自定义滚动容器」降级。
77
+ */
78
+ export const resolveScrollAdapter = (instance: unknown): StackTabsScrollAdapter | null => {
79
+ if (instance == null) return null
80
+ if (isScrollElement(instance)) return { scrollElement: instance }
81
+
82
+ const source = instance as Record<string, unknown>
83
+ const scrollElement = unwrapScrollElement(source.scrollElement)
84
+
85
+ const getPosition =
86
+ typeof source.getPosition === 'function'
87
+ ? () =>
88
+ normalizePosition(
89
+ (
90
+ source.getPosition as (this: unknown) => StackTabsScrollPosition | null | undefined
91
+ ).call(instance)
92
+ )
93
+ : undefined
94
+
95
+ const setPosition =
96
+ typeof source.setPosition === 'function'
97
+ ? (position: StackTabsScrollPosition) => {
98
+ ;(source.setPosition as (this: unknown, value: StackTabsScrollPosition) => void).call(
99
+ instance,
100
+ position
101
+ )
102
+ }
103
+ : undefined
104
+
105
+ const getMetrics =
106
+ typeof source.getMetrics === 'function'
107
+ ? (): StackTabsScrollMetrics | null => {
108
+ const metrics = (
109
+ source.getMetrics as (this: unknown) => StackTabsScrollMetrics | null | undefined
110
+ ).call(instance)
111
+ if (!metrics) return null
112
+ return {
113
+ scrollTop: toFiniteNumber(metrics.scrollTop),
114
+ scrollHeight: toFiniteNumber(metrics.scrollHeight),
115
+ clientHeight: toFiniteNumber(metrics.clientHeight)
116
+ }
117
+ }
118
+ : undefined
119
+
120
+ const onScroll =
121
+ typeof source.onScroll === 'function'
122
+ ? (listener: () => void): (() => void) => {
123
+ const cleanup = (
124
+ source.onScroll as (this: unknown, callback: () => void) => unknown
125
+ ).call(instance, listener)
126
+ return typeof cleanup === 'function' ? (cleanup as () => void) : () => undefined
127
+ }
128
+ : undefined
129
+
130
+ if (!scrollElement && !getPosition && !setPosition && !getMetrics && !onScroll) return null
131
+
132
+ return { scrollElement, getPosition, setPosition, getMetrics, onScroll }
133
+ }
134
+
135
+ /** 适配器是否至少具备一种位置读写能力。 */
136
+ export const canReadScrollPosition = (adapter: StackTabsScrollAdapter | null): boolean =>
137
+ Boolean(adapter && (adapter.getPosition || adapter.scrollElement))
138
+
139
+ /** 读取适配器位置:优先 `getPosition`,缺失时回退原生 `scrollTop / scrollLeft`。 */
140
+ export const readAdapterScrollPosition = (
141
+ adapter: StackTabsScrollAdapter | null
142
+ ): StackTabsScrollPosition | null => {
143
+ if (!adapter) return null
144
+ if (adapter.getPosition) return normalizePosition(adapter.getPosition())
145
+ const element = adapter.scrollElement
146
+ if (!element) return null
147
+ return { top: element.scrollTop, left: element.scrollLeft }
148
+ }
149
+
150
+ /** 写入适配器位置:优先 `setPosition`,缺失时回退原生 `scrollTop / scrollLeft`。 */
151
+ export const writeAdapterScrollPosition = (
152
+ adapter: StackTabsScrollAdapter | null,
153
+ position: StackTabsScrollPosition
154
+ ): boolean => {
155
+ if (!adapter) return false
156
+ if (adapter.setPosition) {
157
+ adapter.setPosition(position)
158
+ return true
159
+ }
160
+ const element = adapter.scrollElement
161
+ if (!element) return false
162
+ element.scrollTop = position.top
163
+ element.scrollLeft = position.left
164
+ return true
165
+ }
166
+
167
+ /** 读取阴影所需尺寸:优先 `getMetrics`,缺失时读取 `scrollElement` 原生属性。 */
168
+ export const readAdapterScrollMetrics = (
169
+ adapter: StackTabsScrollAdapter | null
170
+ ): StackTabsScrollMetrics | null => {
171
+ if (!adapter) return null
172
+ const metrics = adapter.getMetrics?.()
173
+ if (metrics) {
174
+ return {
175
+ scrollTop: toFiniteNumber(metrics.scrollTop),
176
+ scrollHeight: toFiniteNumber(metrics.scrollHeight),
177
+ clientHeight: toFiniteNumber(metrics.clientHeight)
178
+ }
179
+ }
180
+ const element = adapter.scrollElement
181
+ if (!element) return null
182
+ return {
183
+ scrollTop: element.scrollTop,
184
+ scrollHeight: element.scrollHeight,
185
+ clientHeight: element.clientHeight
186
+ }
187
+ }