@ithinkdt/ui 4.0.0-10 → 4.0.0-11

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,322 @@
1
+ import { format } from 'date-fns'
2
+ import {
3
+ NBadge,
4
+ NButton,
5
+ NDrawer,
6
+ NDrawerContent,
7
+ NEmpty,
8
+ NIcon,
9
+ NList,
10
+ NListItem,
11
+ NPagination,
12
+ NThing,
13
+ } from 'ithinkdt-ui'
14
+ import { defineComponent, reactive, withDirectives } from 'vue'
15
+
16
+ import { useAtomicBroadcast } from '@ithinkdt/common/composables'
17
+
18
+ import { vSpin } from '../directives/spin.js'
19
+ import { vTooltip } from '../directives/tooltip.jsx'
20
+ import { useI18n } from '../use-i18n.js'
21
+
22
+ export const AppNotification = /* @__PURE__ */ defineComponent({
23
+ name: 'AppNotification',
24
+ inheritAttrs: false,
25
+ props: {
26
+ getUnreadCount: Function,
27
+ getPage: Function,
28
+ markRead: Function,
29
+ markDelete: Function,
30
+ interval: { type: Number, default: 30_000 },
31
+ },
32
+ setup(props, { slots, attrs }) {
33
+ const { t } = useI18n()
34
+
35
+ const show = ref(false)
36
+ const currentTab = ref('unread')
37
+ const data = reactive({
38
+ page: 1,
39
+ size: 10,
40
+ loading: false,
41
+ records: [],
42
+ total: 0,
43
+ unread: 0,
44
+ })
45
+
46
+ const { sync, post } = useAtomicBroadcast({
47
+ channel: '__ithinkdt_mc_shared_channel',
48
+ onMsg: (msg) => {
49
+ data.unread = msg
50
+ },
51
+ getMsg: () => {
52
+ if (show.value) {
53
+ data.loading = true
54
+ Promise.resolve(props.getPage?.(currentTab.value, data.page, data.size) ?? [])
55
+ .then(({ records, total }) => {
56
+ data.records = records
57
+ if (currentTab.value === 'unread') {
58
+ data.unread = total
59
+ post(total)
60
+ } else {
61
+ data.total = total
62
+ }
63
+ })
64
+ .finally(() => {
65
+ data.loading = false
66
+ })
67
+ }
68
+ return props.getUnreadCount?.() ?? 0
69
+ },
70
+ timeout: () => props.interval,
71
+ immediate: true,
72
+ })
73
+
74
+ return () => {
75
+ const trigger = withDirectives(
76
+ <NButton
77
+ {...attrs}
78
+ quaternary
79
+ style="--n-padding: 0 12px"
80
+ onClick={() => {
81
+ show.value = true
82
+ currentTab.value = data.unread > 0 ? 'unread' : 'all'
83
+ setTimeout(sync, 300)
84
+ }}
85
+ >
86
+ {slots.icon
87
+ ? (
88
+ slots.icon()
89
+ )
90
+ : (
91
+ <NBadge show={data.unread > 0} dot offset={[-2, 5]} processing>
92
+ <NIcon size="18">
93
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
94
+ <g
95
+ fill="none"
96
+ stroke="currentColor"
97
+ stroke-linecap="round"
98
+ stroke-linejoin="round"
99
+ stroke-width="1.5"
100
+ color="currentColor"
101
+ >
102
+ <path d="M2.53 14.77c-.213 1.394.738 2.361 1.902 2.843c4.463 1.85 10.673 1.85 15.136 0c1.164-.482 2.115-1.45 1.902-2.843c-.13-.857-.777-1.57-1.256-2.267c-.627-.924-.689-1.931-.69-3.003C19.525 5.358 16.157 2 12 2S4.475 5.358 4.475 9.5c0 1.072-.062 2.08-.69 3.003c-.478.697-1.124 1.41-1.255 2.267" />
103
+ <path d="M8 19c.458 1.725 2.076 3 4 3c1.925 0 3.541-1.275 4-3" />
104
+ </g>
105
+ </svg>
106
+ </NIcon>
107
+ </NBadge>
108
+ )}
109
+ </NButton>,
110
+ [[vTooltip, t('common.notification.tip', { count: data.unread }), undefined, { bottom: true }]],
111
+ )
112
+
113
+ const header = () => (
114
+ <div style="display: flex; align-items: baseline; gap: 20px">
115
+ <div style="padding-right: 12px">{t('common.notification.title') }</div>
116
+ <NButton
117
+ text
118
+ type={currentTab.value === 'all' ? 'primary' : 'default'}
119
+ onClick={() => {
120
+ currentTab.value = 'all'
121
+ sync()
122
+ }}
123
+ >
124
+ {t('common.notification.all') }
125
+ </NButton>
126
+ <NButton
127
+ text
128
+ type={currentTab.value === 'unread' ? 'primary' : 'default'}
129
+ onClick={() => {
130
+ currentTab.value = 'unread'
131
+ sync()
132
+ }}
133
+ >
134
+ {t('common.notification.unread', { count: data.unread > 99 ? '99+' : data.unread.toString() }) }
135
+ </NButton>
136
+ </div>
137
+ )
138
+
139
+ const renderItem = msg => (
140
+ <NListItem
141
+ key={msg.key}
142
+ onClick={() => {
143
+ if (msg.status === 'unread') {
144
+ props.markRead?.([msg.key]).then(() => {
145
+ msg.status = 'read'
146
+ })
147
+ data.unread--
148
+ }
149
+ if (msg.link) {
150
+ window.open(msg.link, '_target')
151
+ }
152
+ }}
153
+ >
154
+ <NThing
155
+ bordered={false}
156
+ closable
157
+ contentIndented
158
+ size="small"
159
+ onMouseenter={() => (msg.hover = true)}
160
+ onMouseleave={() => (msg.hover = false)}
161
+ style="padding: 0 4px"
162
+ >
163
+ {{
164
+ 'avatar': () => (
165
+ <NIcon size={22} color={msg.status === 'unread' ? 'var(--color-primary)' : undefined}>
166
+ <svg
167
+ xmlns="http://www.w3.org/2000/svg"
168
+ width="1em"
169
+ height="1em"
170
+ viewBox="0 0 24 24"
171
+ >
172
+ <path
173
+ fill="currentColor"
174
+ d="M12.3 7.29c.2-.18.44-.29.7-.29c.27 0 .5.11.71.29c.19.21.29.45.29.71c0 .27-.1.5-.29.71c-.21.19-.44.29-.71.29c-.26 0-.5-.1-.7-.29c-.19-.21-.3-.44-.3-.71c0-.26.11-.5.3-.71m-2.5 4.68s2.17-1.72 2.96-1.79c.74-.06.59.79.52 1.23l-.01.06c-.14.53-.31 1.17-.48 1.78c-.38 1.39-.75 2.75-.66 3c.1.34.72-.09 1.17-.39c.06-.04.11-.08.16-.11c0 0 .08-.08.16.03c.02.03.04.06.06.08c.09.14.14.19.02.27l-.04.02c-.22.15-1.16.81-1.54 1.05c-.41.27-1.98 1.17-1.74-.58c.21-1.23.49-2.29.71-3.12c.41-1.5.59-2.18-.33-1.59c-.37.22-.59.36-.72.45c-.11.08-.12.08-.19-.05l-.03-.06l-.05-.08c-.07-.1-.07-.11.03-.2M22 12c0 5.5-4.5 10-10 10S2 17.5 2 12S6.5 2 12 2s10 4.5 10 10m-2 0c0-4.42-3.58-8-8-8s-8 3.58-8 8s3.58 8 8 8s8-3.58 8-8"
175
+ />
176
+ </svg>
177
+ </NIcon>
178
+ ),
179
+ 'header': () => msg.title,
180
+ 'header-extra': () =>
181
+ msg.hover
182
+ ? (
183
+ <div
184
+ style="display: flex; justify-content: end; gap: 12px"
185
+ onClick={(e) => {
186
+ e.preventDefault()
187
+ e.stopPropagation()
188
+ }}
189
+ >
190
+ {msg.status === 'unread'
191
+ ? (
192
+ <NButton
193
+ text
194
+ type="primary"
195
+ onClick={() => {
196
+ props.markRead?.([msg.key]).then(() => {
197
+ msg.status = 'read'
198
+ data.unread--
199
+ })
200
+ }}
201
+ >
202
+ {t('common.notification.markRead') }
203
+ </NButton>
204
+ )
205
+ : undefined}
206
+
207
+ <NButton
208
+ text
209
+ type="error"
210
+ onClick={() => {
211
+ if (data.records === 1) {
212
+ data.records = []
213
+ sync()
214
+ } else {
215
+ data.records.splice(
216
+ data.records.findIndex(it => it.key == msg.key),
217
+ 1,
218
+ )
219
+ }
220
+ props.markDelete?.([msg.key])
221
+ }}
222
+ >
223
+ {t('common.notification.markDelete') }
224
+ </NButton>
225
+ </div>
226
+ )
227
+ : undefined,
228
+ 'default': msg.content,
229
+ 'footer': () => (
230
+ <span style="color: gray">
231
+ {msg.hover ? format(msg.date, t('common.notification.time')) : t('common.timeago', { time: msg.date }) }
232
+ </span>
233
+ ),
234
+ }}
235
+ </NThing>
236
+ </NListItem>
237
+ )
238
+
239
+ const content = () =>
240
+ withDirectives(
241
+ data.records.length > 0
242
+ ? (
243
+ <NList clickable hoverable style="min-height: 50vh">
244
+ {data.records.map(msg => renderItem(msg))}
245
+ </NList>
246
+ )
247
+ : (
248
+ <NEmpty style="margin-top: 30vh" />
249
+ ),
250
+ [[vSpin, data.loading]],
251
+ )
252
+
253
+ const footer = () => (
254
+ <div style="display: flex; justify-content: space-between; width: 100%">
255
+ {currentTab.value === 'unread'
256
+ ? (
257
+ <NButton
258
+ text
259
+ type="primary"
260
+ disabled={data.records.length === 0}
261
+ onClick={() => {
262
+ const keys = data.records.filter(it => it.status === 'unread').map(it => it.key)
263
+ props.markRead(keys).then(() => {
264
+ while (data.unread - keys.length <= data.size * (data.page - 1)) {
265
+ data.page--
266
+ }
267
+ sync()
268
+ })
269
+ }}
270
+ >
271
+ {{
272
+ icon: () => (
273
+ <NIcon size="20">
274
+ <svg
275
+ xmlns="http://www.w3.org/2000/svg"
276
+ width="1em"
277
+ height="1em"
278
+ viewBox="0 0 24 24"
279
+ >
280
+ <path
281
+ fill="currentColor"
282
+ d="M.41 13.41L6 19l1.41-1.42L1.83 12m20.41-6.42L11.66 16.17L7.5 12l-1.43 1.41L11.66 19l12-12M18 7l-1.41-1.42l-6.35 6.35l1.42 1.41z"
283
+ />
284
+ </svg>
285
+ </NIcon>
286
+ ),
287
+ default: () => t('common.notification.markPageRead'),
288
+ }}
289
+ </NButton>
290
+ )
291
+ : (
292
+ <span />
293
+ )}
294
+ <NPagination
295
+ simple
296
+ pageSize={data.size}
297
+ page={data.page}
298
+ itemCount={currentTab.value === 'unread' ? data.unread : data.total}
299
+ onUpdatePage={(page) => {
300
+ data.page = page
301
+ sync()
302
+ }}
303
+ />
304
+ </div>
305
+ )
306
+ return (
307
+ <>
308
+ {trigger}
309
+ <NDrawer v-model:show={show.value} showMask={false} width={400}>
310
+ <NDrawerContent title={t('common.notification.title')} closable nativeScrollbar={false} bodyContentStyle={{ padding: '0' }}>
311
+ {{
312
+ header,
313
+ default: content,
314
+ footer,
315
+ }}
316
+ </NDrawerContent>
317
+ </NDrawer>
318
+ </>
319
+ )
320
+ }
321
+ },
322
+ })
@@ -0,0 +1,19 @@
1
+ export const clsPrefix = 'app'
2
+
3
+ export const COLLAPSED_INJECTION_KEY = 'APP_SIDER_COLLAPSED'
4
+
5
+ export const IBookmark = () => (
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ role="img"
10
+ width="1em"
11
+ height="1em"
12
+ viewBox="0 0 24 24"
13
+ >
14
+ <path fill="currentColor" d="m17 18l-5-2.18L7 18V5h10m0-2H7a2 2 0 0 0-2 2v16l7-3l7 3V5a2 2 0 0 0-2-2Z" />
15
+ </svg>
16
+ )
17
+
18
+ export const CTX_INJECTION_KEY = '__APP_CTX__'
19
+ export const getCtx = () => inject(CTX_INJECTION_KEY)
@@ -0,0 +1,10 @@
1
+ export { AppContent, AppFooter, AppHeader, AppLayout, AppSider } from './Layout.jsx'
2
+ export { AppLogo } from './Logo.jsx'
3
+ export { AppMenu } from './Menu.jsx'
4
+ export { AppBreadcrumb } from './Breadcrumb.jsx'
5
+ export { AppAccount } from './Account.jsx'
6
+ export { AppFullscreen } from './Fullscreen.jsx'
7
+ export { AppAppearance } from './Appearance.jsx'
8
+ export { AppLanguage } from './Language.jsx'
9
+ export { AppNotification } from './Notification.jsx'
10
+ export { AppMultiTabs } from './MultiTabs.jsx'
@@ -0,0 +1,2 @@
1
+ export { TooltipDirectiveProvider, vTooltip } from './tooltip.jsx'
2
+ export { SpinDirectiveProvider, vSpin } from './spin.js'
@@ -0,0 +1,181 @@
1
+ import { Fragment, defineComponent, h, nextTick, ref, toRef, watch } from 'vue'
2
+
3
+ import { useElementIntersectionRect } from '@ithinkdt/common/composables'
4
+ import { string2dom } from '@ithinkdt/common/dom'
5
+ import { debounce } from '@ithinkdt/common/fn'
6
+
7
+ import useStyle, { c, cB, cE, cM } from '../use-style.js'
8
+
9
+ let clsPrefix
10
+ let isDark
11
+
12
+ const style = /* @__PURE__ */ c([
13
+ c(({ props }) => `:where(${props.bPrefix}spin-host)`, {
14
+ position: 'relative',
15
+ }),
16
+ cB(
17
+ 'spin-directive',
18
+ {
19
+ zIndex: '999999',
20
+ position: 'absolute',
21
+ color: 'var(--color-primary)',
22
+ display: 'flex',
23
+ flexDirection: 'column',
24
+ justifyContent: 'center',
25
+ alignItems: 'center',
26
+ gap: '4px',
27
+ willChange: 'background-color',
28
+ backgroundColor: 'rgb(var(--host-bg, 255 255 255) / 0)',
29
+ transition: 'background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
30
+ },
31
+ [
32
+ cE('tip, icon', {
33
+ willChange: 'opacity',
34
+ opacity: '0',
35
+ transition: 'opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
36
+ }),
37
+ cE('tip:empty', { display: 'none' }),
38
+ cM(
39
+ 'spining',
40
+ {
41
+ backgroundColor: 'rgb(var(--host-bg, 255 255 255) / 0.5)',
42
+ },
43
+ [cE('tip, icon', { opacity: '1' })],
44
+ ),
45
+ ],
46
+ ),
47
+ ])
48
+
49
+ export const SpinDirectiveProvider = /* @__PURE__ */ defineComponent({
50
+ name: 'SpinDirectiveProvider',
51
+ props: {
52
+ dark: Boolean,
53
+ },
54
+ setup(props) {
55
+ clsPrefix = useStyle('-spin-directive', style)
56
+ isDark = toRef(props, 'dark')
57
+ return () => h(Fragment)
58
+ },
59
+ })
60
+
61
+ const Spin = /* @__PURE__ */ Symbol('spin-dir')
62
+
63
+ const updateLoading = (el, { value, modifiers }) => {
64
+ el[Spin].loading.value = !!value
65
+ if (!el[Spin].loading.value) return
66
+ const dark = modifiers.dark || (!modifiers.light && isDark.value)
67
+ el[Spin].spinEl.style.setProperty(
68
+ '--host-bg',
69
+ dark ? '0 0 0' : '255 255 255',
70
+ )
71
+ }
72
+
73
+ export const vSpin = {
74
+ beforeMount(el) {
75
+ el.classList.add(`${clsPrefix.value}-spin-host`)
76
+
77
+ if (el[Spin]) return
78
+
79
+ const loading = ref(false)
80
+
81
+ const spinEl = string2dom(
82
+ `
83
+ <div class="${clsPrefix.value}-spin-directive">
84
+ <svg class="${clsPrefix.value}-spin-directive__icon" width="32" height="32" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
85
+ <g>
86
+ <animateTransform attributeName="transform" type="rotate" values="0 100 100;270 100 100" begin="0s" dur="1.6s" fill="freeze" repeatCount="indefinite"></animateTransform>
87
+ <circle fill="none" stroke="currentColor" stroke-width="18" stroke-linecap="round" cx="100" cy="100" r="92" stroke-dasharray="567" stroke-dashoffset="1848">
88
+ <animateTransform attributeName="transform" type="rotate" values="0 100 100;135 100 100;450 100 100" begin="0s" dur="1.6s" fill="freeze" repeatCount="indefinite"></animateTransform>
89
+ <animate attributeName="stroke-dashoffset" values="567;142;567" begin="0s" dur="1.6s" fill="freeze" repeatCount="indefinite"></animate>
90
+ </circle>
91
+ </g>
92
+ </svg>
93
+ <div class="${clsPrefix.value}-spin-directive__tip"></div>
94
+ </div>
95
+ `.trim(),
96
+ )
97
+
98
+ let rect
99
+ const { stop: stopEl, update } = useElementIntersectionRect(el, (_rect) => {
100
+ rect = _rect
101
+ Object.assign(spinEl.style, {
102
+ left: 0,
103
+ top: 0,
104
+ width: rect.width + 'px',
105
+ height: rect.height + 'px',
106
+ })
107
+ }, { interval: 1000 / 30 })
108
+
109
+ let timer
110
+ const stopSpin = watch(
111
+ loading,
112
+ debounce(() => {
113
+ if (loading.value) {
114
+ if (timer === undefined) {
115
+ clearTimeout(timer)
116
+ timer = undefined
117
+ }
118
+ update()
119
+ el.append(spinEl)
120
+ requestAnimationFrame(() => {
121
+ if (loading.value) spinEl.classList.add(`${clsPrefix.value}-spin-directive--spining`)
122
+ })
123
+ } else {
124
+ spinEl.classList.remove(`${clsPrefix.value}-spin-directive--spining`)
125
+ const listener = () => {
126
+ spinEl.remove()
127
+ if (timer) clearTimeout(timer)
128
+ timer = null
129
+ spinEl.removeEventListener('transitioncancel', listener)
130
+ spinEl.removeEventListener('transitionend', listener)
131
+ }
132
+ spinEl.addEventListener('transitioncancel', listener)
133
+ spinEl.addEventListener('transitionend', listener)
134
+ let timer = setTimeout(listener, 300)
135
+ }
136
+ }, 30),
137
+ )
138
+
139
+ el[Spin] = {
140
+ loading,
141
+ spinEl,
142
+ updateTip: () => {
143
+ const tipEl = spinEl.querySelector(`.${clsPrefix.value}-spin-directive__tip`)
144
+ tipEl.textContent = el.dataset.spinTip
145
+ },
146
+ stop: () => {
147
+ stopEl()
148
+ stopSpin()
149
+ },
150
+ }
151
+ },
152
+
153
+ mounted(el, binding) {
154
+ updateLoading(el, binding)
155
+ el[Spin].updateTip()
156
+ if (el[Spin].observer) return
157
+ // 创建 MutationObserver 对象
158
+ nextTick().then(() => {
159
+ const observer = (el[Spin].observer = new MutationObserver((mutationsList) => {
160
+ for (const mutation of mutationsList) {
161
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-spin-tip') {
162
+ el[Spin].updateTip()
163
+ return
164
+ }
165
+ }
166
+ }))
167
+
168
+ // 开始观察目标元素
169
+ observer.observe(el, { attributes: true, attributeFilter: ['data-spin-tip'] })
170
+ })
171
+ },
172
+
173
+ updated: updateLoading,
174
+
175
+ beforeUnmount(el) {
176
+ el[Spin].loading.value = false
177
+ el[Spin].stop()
178
+ el[Spin].observer?.disconnect()
179
+ delete el[Spin]
180
+ },
181
+ }
@@ -0,0 +1,121 @@
1
+ import { promiseTimeout, useEventListener } from '@vueuse/core'
2
+ import { NTooltip } from 'ithinkdt-ui'
3
+ import { defineComponent, reactive, shallowRef } from 'vue'
4
+
5
+ const Tooltip = /* @__PURE__ */ Symbol('tooltip-dir')
6
+ const current = /* @__PURE__ */ shallowRef()
7
+
8
+ let _update
9
+ export const TooltipDirectiveProvider = /* @__PURE__ */ defineComponent({
10
+ name: 'TooltipDirectiveProvider',
11
+ props: {
12
+ delay: { type: Number, default: 180 },
13
+ },
14
+ setup(props) {
15
+ const tip = shallowRef()
16
+ const data = reactive({ show: false, tip: undefined, placement: 'top', x: 0, y: 0 })
17
+
18
+ _update = async () => {
19
+ if (!current.value) return
20
+ if (props.delay > 0) await promiseTimeout(props.delay)
21
+ const el = current.value
22
+ const tooltip = el?.[Tooltip]
23
+ if (!tooltip) return
24
+ const _tip = tooltip.tip
25
+ tip.value
26
+ = typeof _tip === 'function' ? _tip : () => _tip || el.attributes.getNamedItem('title') || el.textContent
27
+
28
+ const { auto } = tooltip.binding.modifiers
29
+ if (
30
+ !auto
31
+ || el.firstChild.offsetWidth > el.clientWidth
32
+ || el.firstChild.offsetHeight > el.clientHeight
33
+ ) {
34
+ data.show = true
35
+
36
+ const rect = el.getBoundingClientRect()
37
+ data.placement
38
+ = Object.keys(tooltip.binding.modifiers).find(m =>
39
+ ['top', 'right', 'left', 'bottom'].find(k => m.startsWith(k)),
40
+ ) ?? 'top'
41
+ switch (data.placement.split('-')[0]) {
42
+ case 'top': {
43
+ data.x = rect.left + rect.width / 2
44
+ data.y = rect.top
45
+ break
46
+ }
47
+
48
+ case 'right': {
49
+ data.x = rect.left + rect.width
50
+ data.y = rect.top + rect.height / 2
51
+ break
52
+ }
53
+
54
+ case 'left': {
55
+ data.x = rect.left
56
+ data.y = rect.top + rect.height / 2
57
+ break
58
+ }
59
+
60
+ case 'bottom': {
61
+ data.x = rect.left + rect.width / 2
62
+ data.y = rect.bottom
63
+ break
64
+ }
65
+ }
66
+ } else if (data.show) {
67
+ data.show = false
68
+ }
69
+ }
70
+
71
+ useEventListener(
72
+ 'mouseover',
73
+ (ev) => {
74
+ const el = ev.target
75
+ if (!current.value || current.value?.contains(el)) return
76
+ data.show = false
77
+ current.value = undefined
78
+ },
79
+ { capture: true },
80
+ )
81
+
82
+ return () => (
83
+ <NTooltip
84
+ trigger="manual"
85
+ show={!!current.value && data.show}
86
+ x={data.x}
87
+ y={data.y}
88
+ placement={data.placement}
89
+ keepAliveOnHover
90
+ >
91
+ {tip.value?.()}
92
+ </NTooltip>
93
+ )
94
+ },
95
+ })
96
+
97
+ const _listener = (ev) => {
98
+ current.value = ev.target
99
+ _update?.()
100
+ }
101
+ export const vTooltip = {
102
+ mounted(el, binding) {
103
+ el[Tooltip] = {
104
+ binding,
105
+ tip: binding.value,
106
+ }
107
+ el.addEventListener('mouseenter', _listener)
108
+ },
109
+ updated(el, binding) {
110
+ el[Tooltip].binding = binding
111
+ el[Tooltip].tip = binding.value
112
+ _update()
113
+ },
114
+ beforeUnmount(el) {
115
+ if (el[Tooltip]) el.removeEventListener('mouseenter', _listener)
116
+ if (current.value === el) {
117
+ current.value = undefined
118
+ }
119
+ delete el[Tooltip]
120
+ },
121
+ }