@opentiny/vue-hooks 3.20.0 → 3.22.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.
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +24 -0
- package/{src → dist/src}/use-floating.d.ts +7 -2
- package/dist/src/use-floating.js +141 -0
- package/{src → dist/src}/use-lazy-show.d.ts +6 -3
- package/dist/src/use-lazy-show.js +10 -0
- package/dist/src/useEventListener.d.ts +15 -0
- package/dist/src/useEventListener.js +31 -0
- package/dist/src/useInstanceSlots.d.ts +6 -0
- package/dist/src/useInstanceSlots.js +14 -0
- package/dist/src/useRect.d.ts +1 -0
- package/dist/src/useRect.js +18 -0
- package/dist/src/useRelation.d.ts +31 -0
- package/dist/src/useRelation.js +54 -0
- package/dist/src/useTouch.d.ts +15 -0
- package/dist/src/useTouch.js +32 -0
- package/dist/src/useUserAgent.d.ts +3 -0
- package/dist/src/useUserAgent.js +10 -0
- package/dist/src/useWindowSize.d.ts +4 -0
- package/dist/src/useWindowSize.js +14 -0
- package/{index.d.ts → dist/src/vue-emitter.d.ts} +5 -3
- package/dist/src/vue-popper.js +85 -0
- package/dist/src/vue-popup.d.ts +18 -0
- package/dist/src/vue-popup.js +69 -0
- package/index.ts +23 -0
- package/package.json +15 -9
- package/src/use-floating.ts +409 -0
- package/src/use-lazy-show.ts +20 -0
- package/src/useEventListener.ts +65 -0
- package/src/useInstanceSlots.ts +29 -0
- package/src/useRect.ts +25 -0
- package/src/useRelation.ts +130 -0
- package/src/useTouch.ts +74 -0
- package/src/useUserAgent.ts +18 -0
- package/src/useWindowSize.ts +25 -0
- package/src/vue-emitter.ts +49 -0
- package/src/vue-popper.ts +277 -0
- package/src/vue-popup.ts +196 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +23 -0
- package/index.js +0 -2296
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { PopupManager as n, merge as f, isServer as T, addClass as g } from "@opentiny/utils";
|
|
2
|
+
let I = 1;
|
|
3
|
+
const x = ({
|
|
4
|
+
onMounted: l,
|
|
5
|
+
onBeforeUnmount: e,
|
|
6
|
+
watch: p,
|
|
7
|
+
vm: o,
|
|
8
|
+
api: r,
|
|
9
|
+
props: d,
|
|
10
|
+
state: s,
|
|
11
|
+
nextTick: t
|
|
12
|
+
}) => {
|
|
13
|
+
l(() => {
|
|
14
|
+
o._popupId = `popup-${I++}`, n.register(o._popupId, o);
|
|
15
|
+
}), e(() => {
|
|
16
|
+
n.deregister(o._popupId), n.closeModal(o._popupId);
|
|
17
|
+
}), p(
|
|
18
|
+
() => d.visible,
|
|
19
|
+
(i) => {
|
|
20
|
+
if (i) {
|
|
21
|
+
if (o._opening)
|
|
22
|
+
return;
|
|
23
|
+
s.rendered ? r.open() : (s.rendered = !0, t(() => {
|
|
24
|
+
r.open();
|
|
25
|
+
}));
|
|
26
|
+
} else
|
|
27
|
+
r.close();
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}, y = ({ state: l, vm: e }) => (p) => {
|
|
31
|
+
l.rendered || (l.rendered = !0);
|
|
32
|
+
const o = f({}, e.$props || e, p);
|
|
33
|
+
e._closeTimer && (clearTimeout(e._closeTimer), e._closeTimer = null), clearTimeout(e._openTimer);
|
|
34
|
+
const r = () => {
|
|
35
|
+
if (T || l.opened)
|
|
36
|
+
return;
|
|
37
|
+
e._opening = !0;
|
|
38
|
+
const s = e.$el, t = o.modal, i = o.zIndex;
|
|
39
|
+
i && (n.zIndex = i), t && (e._closing && (n.closeModal(e._popupId), e._closing = !1), n.openModal(
|
|
40
|
+
e._popupId,
|
|
41
|
+
n.nextZIndex(),
|
|
42
|
+
o.modalAppendToBody ? void 0 : s,
|
|
43
|
+
o.modalClass,
|
|
44
|
+
o.modalFade
|
|
45
|
+
), o.lockScroll && (n.fixBodyBorder(), g(document.body, n.popLockClass))), getComputedStyle(s).position === "static" && (s.style.position = "absolute"), s.style.zIndex = n.nextZIndex().toString(), l.opened = !0, e._opening = !1;
|
|
46
|
+
}, d = Number(o.openDelay);
|
|
47
|
+
d > 0 ? e._openTimer = setTimeout(() => {
|
|
48
|
+
e._openTimer = null, r();
|
|
49
|
+
}, d) : r();
|
|
50
|
+
}, M = ({ state: l, vm: e }) => () => {
|
|
51
|
+
e._openTimer !== null && (clearTimeout(e._openTimer), e._openTimer = null), clearTimeout(e._closeTimer);
|
|
52
|
+
const p = () => {
|
|
53
|
+
e._closing = !0, l.opened = !1, n.closeModal(e._popupId), e._closing = !1;
|
|
54
|
+
}, o = Number(e.closeDelay);
|
|
55
|
+
o > 0 ? e._closeTimer = setTimeout(() => {
|
|
56
|
+
e._closeTimer = null, p();
|
|
57
|
+
}, o) : p();
|
|
58
|
+
}, m = (l) => {
|
|
59
|
+
const { api: e, nextTick: p, onBeforeUnmount: o, onMounted: r, props: d, reactive: s, toRefs: t, vm: i, watch: u } = l, c = s({
|
|
60
|
+
opened: !1,
|
|
61
|
+
rendered: !1
|
|
62
|
+
});
|
|
63
|
+
x({ onMounted: r, onBeforeUnmount: o, watch: u, vm: i, api: e, props: d, state: c, nextTick: p });
|
|
64
|
+
const a = y({ state: c, vm: i }), _ = M({ state: c, vm: i });
|
|
65
|
+
return { open: a, close: _, PopupManager: n, ...t(c) };
|
|
66
|
+
};
|
|
67
|
+
export {
|
|
68
|
+
m as usePopup
|
|
69
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2022 - present TinyVue Authors.
|
|
3
|
+
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
|
|
4
|
+
*
|
|
5
|
+
* Use of this source code is governed by an MIT-style license.
|
|
6
|
+
*
|
|
7
|
+
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
|
|
8
|
+
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
|
|
9
|
+
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { useFloating } from './src/use-floating'
|
|
14
|
+
export { useLazyShow } from './src/use-lazy-show'
|
|
15
|
+
export { useEventListener } from './src/useEventListener'
|
|
16
|
+
export { useInstanceSlots } from './src/useInstanceSlots'
|
|
17
|
+
export { useRect } from './src/useRect'
|
|
18
|
+
export { useRelation } from './src/useRelation'
|
|
19
|
+
export { useTouch } from './src/useTouch'
|
|
20
|
+
export { useUserAgent } from './src/useUserAgent'
|
|
21
|
+
export { useWindowSize } from './src/useWindowSize'
|
|
22
|
+
export { userPopper } from './src/vue-popper'
|
|
23
|
+
export { usePopup } from './src/vue-popup'
|
package/package.json
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentiny/vue-hooks",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.0",
|
|
4
4
|
"description": "",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"main": "dist/index.js",
|
|
7
9
|
"publishConfig": {
|
|
8
10
|
"access": "public"
|
|
9
11
|
},
|
|
10
12
|
"dependencies": {
|
|
11
13
|
"@floating-ui/dom": "^1.6.9",
|
|
12
|
-
"@opentiny/
|
|
14
|
+
"@opentiny/utils": "~3.22.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "~5.3.3",
|
|
18
|
+
"vite": "^6.0.0",
|
|
19
|
+
"vite-plugin-dts": "~4.3.0",
|
|
20
|
+
"vitest": "^3.0.8"
|
|
13
21
|
},
|
|
14
|
-
"keywords": [],
|
|
15
|
-
"author": "",
|
|
16
|
-
"license": "ISC",
|
|
17
|
-
"types": "index.d.ts",
|
|
18
22
|
"scripts": {
|
|
19
|
-
"
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"pub": "pnpm publish --no-git-checks --access=public",
|
|
25
|
+
"test": "vitest"
|
|
20
26
|
}
|
|
21
27
|
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import type { Placement, Strategy, OffsetOptions, RootBoundary, Boundary, ReferenceElement } from '@floating-ui/dom'
|
|
2
|
+
import { computePosition, autoUpdate, flip, offset, shift, arrow, hide, limitShift } from '@floating-ui/dom'
|
|
3
|
+
|
|
4
|
+
interface IFloatOption {
|
|
5
|
+
reference: null | ReferenceElement
|
|
6
|
+
popper: null | HTMLElement
|
|
7
|
+
/** ✅ 是否弹出 */
|
|
8
|
+
show: boolean
|
|
9
|
+
/** ✅ 是否自动更新位置 */
|
|
10
|
+
autoUpdate: boolean // 更新策略有5种,4种默认为true。 就依默认策略进行update
|
|
11
|
+
/** ✅ 弹出层定位策略, 【不建议修改】: 'absolute' | 'fixed' https://floating-ui.com/docs/computePosition#strategy */
|
|
12
|
+
strategy: Strategy
|
|
13
|
+
/** ✅ 默认出现的12个位置 */
|
|
14
|
+
placement: Placement
|
|
15
|
+
/** ✅ 弹出层偏移量 支持 number | {mainAxis,crossAxis,alignmentAxis}
|
|
16
|
+
* 1、只传入 number, 代表主轴上的偏移。
|
|
17
|
+
* 2、crossAxis,alignmentAxis 都是副轴上的偏移, 区别是:
|
|
18
|
+
* crossAxis 固定向副轴的正方向偏移;
|
|
19
|
+
* alignmentAxis 在副轴上,根据placement的后段决定偏移。
|
|
20
|
+
* 比如 top的副轴为水平方向。 指定alignmentAxis=20的话, top-start时,向右20, top-end时 向左20。
|
|
21
|
+
*/
|
|
22
|
+
offset: OffsetOptions
|
|
23
|
+
/** ✅ 是否显示箭头 */
|
|
24
|
+
arrowVisible: boolean
|
|
25
|
+
/** ✅ 溢出的根边界, 取值为: viewport: 可视视口 document: 整个文档区域 或 自定义Rect:{ x,y,width,height} 【不建议切换,不太确定它影响哪些场景】
|
|
26
|
+
* 在floating 内部, 计算所有 [...clippingAncestors, rootBoundary] 的rect 大小
|
|
27
|
+
* 'viewport' 时,访问的是 window.visualViewport, 其 width是不带滚动条的宽度。
|
|
28
|
+
*/
|
|
29
|
+
rootBoundary: RootBoundary
|
|
30
|
+
/** ✅ 裁剪元素或区域元素。 默认为最近的rel元素。 此处可自定义为某个元素或Rect */
|
|
31
|
+
boundary: Boundary
|
|
32
|
+
/** ✅ 边界预留padding. 设置后,flip 快到边界时,提前就翻转 */
|
|
33
|
+
boundaryPadding: number
|
|
34
|
+
/** ✅ 引用元素不可见时,是否自动隐藏。 【需要启用autoUpdate】 */
|
|
35
|
+
syncHide: boolean
|
|
36
|
+
/** ✅ 元素弹出后,任何重新定位都自动关闭popper, 适用于右键菜单打开后,滚动就或日期组件在滚动时自动关闭。 【需要启用autoUpdate】 */
|
|
37
|
+
autoHide: boolean
|
|
38
|
+
/** ✅ 是否加速。 加速时,绑定popper的translate属性,否则绑定left/top。 【该属性不建议切换】 */
|
|
39
|
+
gpuAcceleration: boolean
|
|
40
|
+
/** ✅ 是否动画。 动画的机制简化, 不考虑前个动画未结束时,就开始下个动画的情况。 */
|
|
41
|
+
animate: boolean
|
|
42
|
+
/** ✅ 动画类名 */
|
|
43
|
+
animateName: string
|
|
44
|
+
/** ✅ 是否添加到body。【该属性不建议切换】
|
|
45
|
+
* true时, 显示popper时,才body.append; 隐藏时popper.remove。 boundary为 body.
|
|
46
|
+
* false时, 显示popper, 修改style.display='block', 隐藏修改 display:none boundary为 最近的relative元素 */
|
|
47
|
+
appendToBody: boolean
|
|
48
|
+
/** ✅ 自定义类名,以支持不同的主题色, is-dark is-light 等 , 支持空格分隔的多个类名 */
|
|
49
|
+
customClass: string
|
|
50
|
+
|
|
51
|
+
/** 是否启用flip flip, shift 属性会影响弹层的位置。 在鼠标右击菜单等场景,想固定弹出位置时,可以关闭该属性 */
|
|
52
|
+
flipAble: boolean
|
|
53
|
+
/** 是否启用shift */
|
|
54
|
+
shiftAble: boolean
|
|
55
|
+
|
|
56
|
+
/** 缓存上次的值。 由于watch state时,取不到oldState的值,所以每次应用后,记录一下 */
|
|
57
|
+
_last: Partial<IFloatOption> & {
|
|
58
|
+
arrowInserted?: boolean
|
|
59
|
+
arrowEl: HTMLElement
|
|
60
|
+
timestamp: number
|
|
61
|
+
}
|
|
62
|
+
/** 缓存用户注册事件
|
|
63
|
+
* show 事件:如果useFloating时,show=true, 那么监听不到第一次show事件。 因为第一次show事件在usFloating内部就已经触发了
|
|
64
|
+
* hide 事件:在动画结束后触发。【是否增加hiding 事件?】
|
|
65
|
+
* update 事件: 每次定位完后触发。 该事件触发频繁,已观察到有以下情况:
|
|
66
|
+
* 在 autoUpdate 时,会频繁触发。 比如切换显示,elementResize /IntersectionObserver 事件发生,内部会进入2次
|
|
67
|
+
* 在reference 不可见时,每一秒会触发一次 update
|
|
68
|
+
* */
|
|
69
|
+
_events: { show: Function[]; hide: Function[]; update: Function[] }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 默认配置 */
|
|
73
|
+
const defaultOption: Partial<IFloatOption> = {
|
|
74
|
+
reference: null,
|
|
75
|
+
popper: null,
|
|
76
|
+
show: false,
|
|
77
|
+
autoUpdate: true,
|
|
78
|
+
|
|
79
|
+
strategy: 'absolute',
|
|
80
|
+
placement: 'bottom',
|
|
81
|
+
offset: 6,
|
|
82
|
+
arrowVisible: true,
|
|
83
|
+
rootBoundary: 'viewport',
|
|
84
|
+
boundary: 'clippingAncestors',
|
|
85
|
+
boundaryPadding: 5,
|
|
86
|
+
syncHide: true,
|
|
87
|
+
autoHide: false,
|
|
88
|
+
gpuAcceleration: false,
|
|
89
|
+
animate: true,
|
|
90
|
+
animateName: 'fade-in-linear',
|
|
91
|
+
appendToBody: false,
|
|
92
|
+
customClass: '',
|
|
93
|
+
|
|
94
|
+
flipAble: true,
|
|
95
|
+
shiftAble: true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const oppositeSidesMap = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }
|
|
99
|
+
|
|
100
|
+
const toMs = (s: string) => {
|
|
101
|
+
if (s === 'auto') return 0
|
|
102
|
+
return Number(s.slice(0, -1).replace(',', '.')) * 1000
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 获取元素的当前动画时长,参考 Vue的Transition 的源码实现。 注:无论css中单位是 ms/s, getComputedStyle返回的单位都是 s */
|
|
106
|
+
const getTransitionInfo = (el: HTMLElement) => {
|
|
107
|
+
const styles = window.getComputedStyle(el)
|
|
108
|
+
// 先判断transition
|
|
109
|
+
let timeout = toMs(styles.transitionDelay) + toMs(styles.transitionDuration)
|
|
110
|
+
if (timeout) return timeout
|
|
111
|
+
|
|
112
|
+
// 再判断 animation
|
|
113
|
+
timeout = toMs(styles.animationDelay) + toMs(styles.animationDuration)
|
|
114
|
+
if (timeout) return timeout
|
|
115
|
+
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 包含多个类名的字符串赋值给元素的classList */
|
|
120
|
+
const applyClass = (el: HTMLElement, classes: string, force: boolean) => {
|
|
121
|
+
classes.split(' ').forEach((c) => c && el.classList.toggle(c, force))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 执行一次 popper 的更新动作 */
|
|
125
|
+
const updatePopper = (state: IFloatOption) => {
|
|
126
|
+
// 官方建议offset居首, flip在shift前。 arrow,hide居后。
|
|
127
|
+
const middleware = [offset(state.offset)]
|
|
128
|
+
state.flipAble &&
|
|
129
|
+
middleware.push(
|
|
130
|
+
flip({
|
|
131
|
+
rootBoundary: state.rootBoundary,
|
|
132
|
+
boundary: state.boundary,
|
|
133
|
+
padding: state.boundaryPadding
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
state.shiftAble && middleware.push(shift({ limiter: limitShift() }))
|
|
137
|
+
state.arrowVisible &&
|
|
138
|
+
middleware.push(
|
|
139
|
+
arrow({
|
|
140
|
+
element: state.popper!.querySelector('.tiny-popper__arrow')!,
|
|
141
|
+
padding: 8
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
middleware.push(hide())
|
|
145
|
+
|
|
146
|
+
computePosition(state.reference!, state.popper!, {
|
|
147
|
+
placement: state.placement,
|
|
148
|
+
strategy: state.strategy,
|
|
149
|
+
middleware
|
|
150
|
+
}).then(({ x, y, placement, strategy, middlewareData }) => {
|
|
151
|
+
// 自动关闭: 如果已经打开状态了,则本次重新定位,则关闭
|
|
152
|
+
if (state.autoHide && state._last.show) {
|
|
153
|
+
const timestamp = new Date().getTime()
|
|
154
|
+
if (timestamp > state._last.timestamp + 300) {
|
|
155
|
+
state.show = false
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// 最终绑定给popper的样式
|
|
160
|
+
const finalStyles: Record<string, string> = {}
|
|
161
|
+
|
|
162
|
+
// 定位策略
|
|
163
|
+
Object.assign(finalStyles, {
|
|
164
|
+
position: strategy
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// 位置:是否加速
|
|
168
|
+
if (state.gpuAcceleration) {
|
|
169
|
+
Object.assign(finalStyles, {
|
|
170
|
+
transform: `translate(${x}px,${y}px)`,
|
|
171
|
+
left: '0',
|
|
172
|
+
top: '0'
|
|
173
|
+
})
|
|
174
|
+
} else {
|
|
175
|
+
Object.assign(finalStyles, {
|
|
176
|
+
left: `${x}px`,
|
|
177
|
+
top: `${y}px`
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 是否hide
|
|
182
|
+
if (state.syncHide) {
|
|
183
|
+
if (middlewareData.hide) {
|
|
184
|
+
Object.assign(finalStyles, {
|
|
185
|
+
visibility: middlewareData.hide.referenceHidden ? 'hidden' : 'visible'
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 应用样式
|
|
191
|
+
Object.assign(state.popper!.style, finalStyles)
|
|
192
|
+
|
|
193
|
+
// 应用customClass
|
|
194
|
+
if (state._last.customClass && state._last.customClass !== state.customClass) {
|
|
195
|
+
applyClass(state.popper!, state._last.customClass, false)
|
|
196
|
+
}
|
|
197
|
+
if (state.customClass && state._last?.customClass !== state.customClass) {
|
|
198
|
+
applyClass(state.popper!, state.customClass, true)
|
|
199
|
+
state._last.customClass = state.customClass
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 应用箭头
|
|
203
|
+
if (state.arrowVisible) {
|
|
204
|
+
const { x: arrowX, y: arrowY } = middlewareData.arrow
|
|
205
|
+
const arrowElement = state._last.arrowEl!
|
|
206
|
+
const staticSide = oppositeSidesMap[placement.split('-')[0]]
|
|
207
|
+
|
|
208
|
+
const arrowStyle = {
|
|
209
|
+
left: arrowX !== null ? `${arrowX}px` : '',
|
|
210
|
+
top: arrowY !== null ? `${arrowY}px` : '',
|
|
211
|
+
right: '',
|
|
212
|
+
bottom: '',
|
|
213
|
+
[staticSide]: '-4px',
|
|
214
|
+
display: 'block'
|
|
215
|
+
}
|
|
216
|
+
Object.assign(arrowElement.style, arrowStyle)
|
|
217
|
+
} else {
|
|
218
|
+
if (state._last!.arrowInserted) {
|
|
219
|
+
state._last!.arrowEl.style.display = 'none'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 触发更新事件
|
|
224
|
+
emit(state, 'update', { x, y, placement, strategy, middlewareData })
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 执行自动更新 */
|
|
229
|
+
const autoUpdatePopper = (state: IFloatOption) => {
|
|
230
|
+
return autoUpdate(state.reference!, state.popper!, () => {
|
|
231
|
+
updatePopper(state)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** popper 插入body,或修改 display 可见。 */
|
|
236
|
+
const appendPopper = (state: IFloatOption) => {
|
|
237
|
+
// 如果已经打开了,且popper没变化,则忽略
|
|
238
|
+
if (state._last.show && state._last.popper === state.popper) return
|
|
239
|
+
|
|
240
|
+
// 如果popper 变化了, 需要先移除_last.popper。
|
|
241
|
+
if (state._last.popper && state._last.popper !== state.popper) {
|
|
242
|
+
if (state._last.appendToBody) {
|
|
243
|
+
state._last.popper.remove()
|
|
244
|
+
} else {
|
|
245
|
+
state._last.popper.style.display = 'none'
|
|
246
|
+
}
|
|
247
|
+
state._last.arrowInserted = false
|
|
248
|
+
state._last.arrowEl = null as unknown as HTMLElement
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (state.popper) {
|
|
252
|
+
// 1、插入元素
|
|
253
|
+
if (state.appendToBody) {
|
|
254
|
+
document.body.append(state.popper)
|
|
255
|
+
} else {
|
|
256
|
+
state.popper.style.display = 'block'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 2、始终插入箭头元素,update时控制箭头的显隐。(如果不插入,只动态修改arrowVisible,进入不了appendPopper)
|
|
260
|
+
if (!state._last!.arrowInserted) {
|
|
261
|
+
const arrowEl = document.createElement('div')
|
|
262
|
+
arrowEl.className = 'tiny-popper__arrow'
|
|
263
|
+
state.popper.append(arrowEl)
|
|
264
|
+
|
|
265
|
+
state._last!.arrowInserted = true
|
|
266
|
+
state._last!.arrowEl = arrowEl
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3、 添加动画类
|
|
270
|
+
if (state.animate) {
|
|
271
|
+
const enterName = `${state.animateName}-enter-from`
|
|
272
|
+
const activeName = `${state.animateName}-enter-active`
|
|
273
|
+
state.popper.classList.add(enterName, activeName)
|
|
274
|
+
setTimeout(() => {
|
|
275
|
+
state.popper!.classList.remove(enterName)
|
|
276
|
+
}, 0)
|
|
277
|
+
const timeout = getTransitionInfo(state.popper)
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
state.popper!.classList.remove(activeName)
|
|
280
|
+
}, timeout)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 4、触发事件
|
|
284
|
+
emit(state, 'show')
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** popper 移除body,或修改 display 不可见 */
|
|
289
|
+
const closePopper = (state: IFloatOption) => {
|
|
290
|
+
// 如果已经关闭了,则忽略
|
|
291
|
+
if (!state._last.show) return
|
|
292
|
+
|
|
293
|
+
if (state.popper) {
|
|
294
|
+
// 如果有动画,动画结束后再移除
|
|
295
|
+
if (state.animate && state.animateName) {
|
|
296
|
+
const leaveName = `${state.animateName}-leave-to`
|
|
297
|
+
const activeName = `${state.animateName}-leave-active`
|
|
298
|
+
|
|
299
|
+
state.popper.classList.add(leaveName, activeName)
|
|
300
|
+
const timeout = getTransitionInfo(state.popper)
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
state.popper!.classList.remove(leaveName, activeName)
|
|
303
|
+
|
|
304
|
+
if (state.appendToBody) {
|
|
305
|
+
state.popper!.remove()
|
|
306
|
+
} else {
|
|
307
|
+
state.popper!.style.display = 'none'
|
|
308
|
+
}
|
|
309
|
+
emit(state, 'hide')
|
|
310
|
+
}, timeout)
|
|
311
|
+
} else {
|
|
312
|
+
// 否则直接移除
|
|
313
|
+
if (state.appendToBody) {
|
|
314
|
+
state.popper.remove()
|
|
315
|
+
} else {
|
|
316
|
+
state.popper.style.display = 'none'
|
|
317
|
+
}
|
|
318
|
+
emit(state, 'hide')
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** 触发事件 */
|
|
324
|
+
const emit = (state: IFloatOption, eventName: string, params?: any) => {
|
|
325
|
+
state._events[eventName].forEach((cb) => cb(params))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 快速构建虚拟元素的辅助方法, 适于右键菜单,区域选择, 跟随光标等场景 */
|
|
329
|
+
const virtualEl = (x: number, y: number, w = 0, h = 0) => ({
|
|
330
|
+
getBoundingClientRect() {
|
|
331
|
+
return {
|
|
332
|
+
width: 0,
|
|
333
|
+
height: 0,
|
|
334
|
+
x,
|
|
335
|
+
y,
|
|
336
|
+
top: y,
|
|
337
|
+
left: x,
|
|
338
|
+
right: x + w,
|
|
339
|
+
bottom: y + h
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
/** 响应式的弹出层管理函数,适用于场景: tooltip, poppover, select, 右键菜单, floatbar, notify, 或 canvas上跟随鼠标等 */
|
|
345
|
+
export const useFloating =
|
|
346
|
+
({ reactive, watch, markRaw, onBeforeUnmount }) =>
|
|
347
|
+
(option: Partial<IFloatOption> = {}) => {
|
|
348
|
+
const state = reactive(option) as IFloatOption
|
|
349
|
+
|
|
350
|
+
let cleanup: null | (() => void) = null
|
|
351
|
+
|
|
352
|
+
// 0、标准化state
|
|
353
|
+
Object.keys(defaultOption).forEach((key) => {
|
|
354
|
+
if (!Object.prototype.hasOwnProperty.call(state, key)) {
|
|
355
|
+
state[key] = defaultOption[key]
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
state._last = markRaw({}) as any
|
|
359
|
+
state._events = markRaw({ show: [], hide: [], update: [] })
|
|
360
|
+
|
|
361
|
+
const watchState = () => {
|
|
362
|
+
// 1、引用和弹窗同时存在
|
|
363
|
+
if (state.popper && state.reference) {
|
|
364
|
+
// 1.1 当前需要显示, 可能是show变化了,也可能是其它任意值变化了, 都需要重新的一次update
|
|
365
|
+
if (state.show) {
|
|
366
|
+
appendPopper(state)
|
|
367
|
+
if (state.autoUpdate) {
|
|
368
|
+
cleanup && cleanup()
|
|
369
|
+
cleanup = autoUpdatePopper(state)
|
|
370
|
+
} else {
|
|
371
|
+
updatePopper(state)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// 1.2 当前不需要显示
|
|
375
|
+
else {
|
|
376
|
+
cleanup && cleanup()
|
|
377
|
+
closePopper(state)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// 2、引用和弹窗不全。 可能前一次是全的,所以要释放一下
|
|
381
|
+
else {
|
|
382
|
+
cleanup && cleanup()
|
|
383
|
+
closePopper(state)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
state._last.popper = state.popper
|
|
387
|
+
state._last.reference = state.reference
|
|
388
|
+
state._last.show = (state.show && state.popper && state.reference) as boolean // 真实的是否show变量
|
|
389
|
+
state._last.appendToBody = state.appendToBody
|
|
390
|
+
state._last.timestamp = new Date().getTime()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
watch(state, watchState, { immediate: true })
|
|
394
|
+
|
|
395
|
+
const on = (eventName, cb) => state._events[eventName].push(cb)
|
|
396
|
+
const off = (eventName, cb) => (state._events[eventName] = state._events[eventName].filter((i) => i !== cb))
|
|
397
|
+
|
|
398
|
+
// 3、组件卸载前,移除元素
|
|
399
|
+
onBeforeUnmount(() => {
|
|
400
|
+
cleanup && cleanup()
|
|
401
|
+
closePopper(state)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// 4、返回state 及辅助方法
|
|
405
|
+
// 正常修改state去触发更新,但如果某些业务想在state不变时,仍想执行一次更新, 则使用forceUpdate即可
|
|
406
|
+
// 比如select 懒加载: popper, show都不变, 但popper 的大小变化了,可以forceUpdate一下。
|
|
407
|
+
// 【autoUpdate 理论上会监听 popper的resize的, 这层考虑可能是多余。】
|
|
408
|
+
return { state, on, off, virtualEl, forceUpdate: watchState }
|
|
409
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 慢加载的 v-show 的办法, 灵感来自于: https://github.com/antfu/v-lazy-show
|
|
2
|
+
* 适用场景: 存在初始加载时,不需要显示的区域,但又需要用v-show切换显示。 比如 tabs\collapse\dropdown\cascader\carousel等
|
|
3
|
+
* @example
|
|
4
|
+
* const isShow = ref(false)
|
|
5
|
+
* const { lazyShow: lazyShowPopper } = useLazyShow(isShow)
|
|
6
|
+
*
|
|
7
|
+
* <div v-if="lazyShowPopper" v-show="isShow">
|
|
8
|
+
* isShow 第一次为true时,才会加载该DOM
|
|
9
|
+
* </div>
|
|
10
|
+
*/
|
|
11
|
+
export const useLazyShow =
|
|
12
|
+
({ ref, watch, isRef }) =>
|
|
13
|
+
(show) => {
|
|
14
|
+
const lazyShow = ref(isRef(show) ? show.value : show)
|
|
15
|
+
|
|
16
|
+
if (!lazyShow.value) {
|
|
17
|
+
const stop = watch(show, (v) => v && (lazyShow.value = true) && stop(), { flush: 'sync' })
|
|
18
|
+
}
|
|
19
|
+
return { lazyShow }
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { on, off, isServer } from '@opentiny/utils'
|
|
2
|
+
|
|
3
|
+
export const onMountedOrActivated =
|
|
4
|
+
({ onMounted, onActivated, nextTick }) =>
|
|
5
|
+
(hook) => {
|
|
6
|
+
let mounted
|
|
7
|
+
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
hook()
|
|
10
|
+
nextTick(() => (mounted = true))
|
|
11
|
+
})
|
|
12
|
+
onActivated(() => mounted && hook())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useEventListener =
|
|
16
|
+
({ unref, isRef, watch, nextTick, onMounted, onUnmounted, onActivated, onDeactivated }) =>
|
|
17
|
+
(type, listener, options = {}) => {
|
|
18
|
+
if (isServer) return
|
|
19
|
+
|
|
20
|
+
const { target = window, passive = false, capture = false } = options
|
|
21
|
+
|
|
22
|
+
let cleaned = false
|
|
23
|
+
let attached
|
|
24
|
+
|
|
25
|
+
const add = (target) => {
|
|
26
|
+
if (cleaned) return
|
|
27
|
+
|
|
28
|
+
const element = unref(target)
|
|
29
|
+
|
|
30
|
+
if (element && !attached) {
|
|
31
|
+
on(element, type, listener, { capture, passive })
|
|
32
|
+
attached = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const remove = (target) => {
|
|
37
|
+
if (cleaned) return
|
|
38
|
+
|
|
39
|
+
const element = unref(target)
|
|
40
|
+
|
|
41
|
+
if (element && attached) {
|
|
42
|
+
off(element, type, listener, { capture, passive })
|
|
43
|
+
attached = false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onUnmounted(() => remove(target))
|
|
48
|
+
onDeactivated(() => remove(target))
|
|
49
|
+
onMountedOrActivated({ onMounted, onActivated, nextTick })(() => add(target))
|
|
50
|
+
|
|
51
|
+
let stopWatch
|
|
52
|
+
|
|
53
|
+
if (isRef(target)) {
|
|
54
|
+
stopWatch = watch(target, (val, oldVal) => {
|
|
55
|
+
remove(oldVal)
|
|
56
|
+
add(val)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
stopWatch && stopWatch()
|
|
62
|
+
remove(target)
|
|
63
|
+
cleaned = true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const useInstanceSlots =
|
|
2
|
+
({ getCurrentInstance, isVue2, nextTick, onUnmounted }) =>
|
|
3
|
+
() => {
|
|
4
|
+
const publicInstance = getCurrentInstance().proxy
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 在 Vue2,$scopedSlots 内容是插槽方法,$slots 内容是执行后的虚拟节点,使用 $slots 实践中发现会导致插槽丢失响应性,应该使用 $scopedSlots
|
|
8
|
+
* 在 Vue3,$scopedSlots 是 undefined,$slots 内容是插槽方法,在渲染函数中使用 undefined 的 $scopedSlots 会出现警告提示
|
|
9
|
+
* 为了兼容 Vue2 和 Vue3,以及消除警告提示,这里在 Vue3 实例上定义 $scopedSlots 为 null
|
|
10
|
+
*/
|
|
11
|
+
if (!isVue2) {
|
|
12
|
+
Object.defineProperty(publicInstance, '$scopedSlots', { configurable: true, value: null })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Object.defineProperty(publicInstance, 'instanceSlots', {
|
|
16
|
+
configurable: true,
|
|
17
|
+
get: () => publicInstance.$scopedSlots || publicInstance.$slots
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
onUnmounted(() => {
|
|
21
|
+
nextTick(() => {
|
|
22
|
+
if (!isVue2) {
|
|
23
|
+
delete publicInstance.$scopedSlots
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
delete publicInstance.instanceSlots
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
}
|
package/src/useRect.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const isWindow = (val) => val === window
|
|
2
|
+
const makeDOMRect = (width, height) => ({
|
|
3
|
+
top: 0,
|
|
4
|
+
left: 0,
|
|
5
|
+
width,
|
|
6
|
+
right: width,
|
|
7
|
+
height,
|
|
8
|
+
bottom: height
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const useRect = (unref) => (elOrRef) => {
|
|
12
|
+
const el = unref(elOrRef)
|
|
13
|
+
|
|
14
|
+
if (isWindow(el)) {
|
|
15
|
+
const width = el.innerWidth
|
|
16
|
+
const height = el.innerHeight
|
|
17
|
+
return makeDOMRect(width, height)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (el && el.getBoundingClientRect) {
|
|
21
|
+
return el.getBoundingClientRect()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return makeDOMRect(0, 0)
|
|
25
|
+
}
|