vue-stack-tabs 2.5.1 → 2.6.1
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/StackTabs.vue.d.ts +9 -1
- package/dist/hooks/stackTabsContext.d.ts +8 -1
- package/dist/hooks/tabPanel/pageComponentFactory.d.ts +3 -0
- package/dist/hooks/tabPanel/scroll.d.ts +16 -0
- package/dist/hooks/useScrollShadow.d.ts +20 -2
- package/dist/hooks/useTabPanel.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/model/ScrollAdapter.d.ts +60 -0
- package/dist/vue-stack-tabs.es.js +1 -1
- package/package.json +1 -1
- package/src/lib/StackTabs.vue +17 -1
- package/src/lib/hooks/stackTabsContext.ts +11 -2
- package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +124 -16
- package/src/lib/hooks/tabPanel/scroll.ts +110 -7
- package/src/lib/hooks/useScrollShadow.ts +69 -25
- package/src/lib/hooks/useTabPanel.tsx +14 -3
- package/src/lib/index.ts +6 -0
- package/src/lib/model/ScrollAdapter.ts +187 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ITabBase, ITabData, ITabItem, ITabPage } from '../model/TabModel'
|
|
15
15
|
import { computed, ref } from 'vue'
|
|
16
|
-
import type { DefineComponent, VNode } from 'vue'
|
|
16
|
+
import type { Component, DefineComponent, VNode } from 'vue'
|
|
17
17
|
import { isNavigationFailure, useRouter } from 'vue-router'
|
|
18
18
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
19
19
|
import { encodeTabInfo, createId, createPageId, decodeTabInfo } from '../utils/tabInfoEncoder'
|
|
@@ -108,8 +108,14 @@ const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
|
|
|
108
108
|
restoreCaches,
|
|
109
109
|
reset: resetStore
|
|
110
110
|
} = store
|
|
111
|
-
const {
|
|
112
|
-
|
|
111
|
+
const {
|
|
112
|
+
restoreScroller,
|
|
113
|
+
saveScroller,
|
|
114
|
+
recordMainScroller,
|
|
115
|
+
removeScroller,
|
|
116
|
+
addPageScroller,
|
|
117
|
+
setPageScrollAdapter
|
|
118
|
+
} = createTabPanelScroll(runtimeContext)
|
|
113
119
|
const {
|
|
114
120
|
saveActiveTabToSession,
|
|
115
121
|
clearSession,
|
|
@@ -246,7 +252,9 @@ const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
|
|
|
246
252
|
evictMarkedCaches,
|
|
247
253
|
clearTabEvictionMarks,
|
|
248
254
|
addPageScroller,
|
|
255
|
+
setPageScrollAdapter,
|
|
249
256
|
saveScroller,
|
|
257
|
+
recordMainScroller,
|
|
250
258
|
restoreScroller,
|
|
251
259
|
removeScroller
|
|
252
260
|
})
|
|
@@ -813,6 +821,9 @@ const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
|
|
|
813
821
|
setGlobalScroll: (value: boolean) => {
|
|
814
822
|
runtimeContext.useGlobalScroll.value = value
|
|
815
823
|
},
|
|
824
|
+
setScrollContainer: (value: Component | null | undefined) => {
|
|
825
|
+
runtimeContext.scrollContainer.value = value ?? null
|
|
826
|
+
},
|
|
816
827
|
clearSession,
|
|
817
828
|
setSessionPrefix: (value: string) => {
|
|
818
829
|
runtimeContext.sessionPrefix.value = value
|
package/src/lib/index.ts
CHANGED
|
@@ -37,6 +37,12 @@ export {
|
|
|
37
37
|
}
|
|
38
38
|
export { postOpenTab, onRefreshRequest, MSG_REFRESH, MSG_OPEN_TAB } from './utils/iframeBridge'
|
|
39
39
|
export type { IframeOpenTabPayload } from './utils/iframeBridge'
|
|
40
|
+
export type {
|
|
41
|
+
StackTabsScrollAdapter,
|
|
42
|
+
StackTabsScrollElementInstance,
|
|
43
|
+
StackTabsScrollMetrics,
|
|
44
|
+
StackTabsScrollPosition
|
|
45
|
+
} from './model/ScrollAdapter'
|
|
40
46
|
export default {
|
|
41
47
|
install(Vue: App, options?: VueStackTabsPluginOptions): void {
|
|
42
48
|
logVersion(import.meta.env.PACKAGE_VERSION)
|
|
@@ -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
|
+
}
|