@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.
Files changed (44) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +3 -0
  3. package/dist/index.d.ts +22 -0
  4. package/dist/index.js +24 -0
  5. package/{src → dist/src}/use-floating.d.ts +7 -2
  6. package/dist/src/use-floating.js +141 -0
  7. package/{src → dist/src}/use-lazy-show.d.ts +6 -3
  8. package/dist/src/use-lazy-show.js +10 -0
  9. package/dist/src/useEventListener.d.ts +15 -0
  10. package/dist/src/useEventListener.js +31 -0
  11. package/dist/src/useInstanceSlots.d.ts +6 -0
  12. package/dist/src/useInstanceSlots.js +14 -0
  13. package/dist/src/useRect.d.ts +1 -0
  14. package/dist/src/useRect.js +18 -0
  15. package/dist/src/useRelation.d.ts +31 -0
  16. package/dist/src/useRelation.js +54 -0
  17. package/dist/src/useTouch.d.ts +15 -0
  18. package/dist/src/useTouch.js +32 -0
  19. package/dist/src/useUserAgent.d.ts +3 -0
  20. package/dist/src/useUserAgent.js +10 -0
  21. package/dist/src/useWindowSize.d.ts +4 -0
  22. package/dist/src/useWindowSize.js +14 -0
  23. package/{index.d.ts → dist/src/vue-emitter.d.ts} +5 -3
  24. package/dist/src/vue-popper.js +85 -0
  25. package/dist/src/vue-popup.d.ts +18 -0
  26. package/dist/src/vue-popup.js +69 -0
  27. package/index.ts +23 -0
  28. package/package.json +15 -9
  29. package/src/use-floating.ts +409 -0
  30. package/src/use-lazy-show.ts +20 -0
  31. package/src/useEventListener.ts +65 -0
  32. package/src/useInstanceSlots.ts +29 -0
  33. package/src/useRect.ts +25 -0
  34. package/src/useRelation.ts +130 -0
  35. package/src/useTouch.ts +74 -0
  36. package/src/useUserAgent.ts +18 -0
  37. package/src/useWindowSize.ts +25 -0
  38. package/src/vue-emitter.ts +49 -0
  39. package/src/vue-popper.ts +277 -0
  40. package/src/vue-popup.ts +196 -0
  41. package/tsconfig.json +25 -0
  42. package/tsconfig.node.json +10 -0
  43. package/vite.config.ts +23 -0
  44. 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.20.0",
3
+ "version": "3.22.0",
4
4
  "description": "",
5
- "module": "./lib/index.js",
6
- "main": "./lib/index.js",
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/vue-common": "~3.20.0"
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
- "test": "echo \"Error: no test specified\" && exit 1"
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
+ }