@h-ai/ui 0.1.0-alpha.48 → 0.1.0-alpha.50
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 +20 -3
- package/dist/components/compounds/Pagination.svelte +12 -6
- package/dist/components/compounds/Popover.svelte +104 -18
- package/dist/components/compounds/Tabs.svelte +20 -1
- package/dist/components/compounds/Tooltip.svelte +127 -6
- package/dist/components/scenes/crud/CrudPage.svelte +167 -90
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +13 -0
- package/dist/components/scenes/crud/crud-controller.svelte.d.ts +213 -0
- package/dist/components/scenes/crud/crud-controller.svelte.js +265 -0
- package/dist/components/scenes/crud/index.d.ts +5 -1
- package/dist/components/scenes/crud/index.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/internal/browser-safety.d.ts +3 -2
- package/dist/internal/browser-safety.js +26 -5
- package/dist/messages/en-US.json +3 -1
- package/dist/messages/zh-CN.json +3 -1
- package/dist/toast.svelte.d.ts +46 -0
- package/dist/toast.svelte.js +61 -0
- package/dist/types.d.ts +27 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ components/
|
|
|
174
174
|
| `Modal` | 模态框 | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
|
|
175
175
|
| `Drawer` | 抽屉 | `open`, `position`, `title`, `size`, `width`, `resizable`, `widthStorageKey` |
|
|
176
176
|
| `Confirm` | 确认框 | `open`, `title`, `message`, `variant`, `onconfirm` |
|
|
177
|
-
| `Popover` | 弹出层 | `open`, `position`, `trigger`, `offset`
|
|
177
|
+
| `Popover` | 弹出层 | `open`, `position`, `trigger`, `offset`, `portal` |
|
|
178
178
|
|
|
179
179
|
#### 数据展示
|
|
180
180
|
|
|
@@ -211,7 +211,7 @@ components/
|
|
|
211
211
|
|
|
212
212
|
| 组件 | 描述 | 主要属性 |
|
|
213
213
|
| ------------ | -------- | -------------------------------------------- |
|
|
214
|
-
| `Tabs` | 标签页 | `items`, `active`, `type`, `size`
|
|
214
|
+
| `Tabs` | 标签页 | `items`, `active`, `type`, `size`, `lazy`, `onactivate` |
|
|
215
215
|
| `Pagination` | 分页(统一 table 风格) | `page`, `total`, `pageSize`, `showTotal`, `showJumper`, `showSizeChanger`, `showPageInfo`, `showFirstLast` |
|
|
216
216
|
| `Breadcrumb` | 面包屑 | `items`, `separator` |
|
|
217
217
|
| `Steps` | 步骤条 | `items`, `current`, `direction`, `clickable` |
|
|
@@ -334,7 +334,7 @@ components/
|
|
|
334
334
|
|
|
335
335
|
| 组件 | 描述 | 主要属性 / 能力 |
|
|
336
336
|
| -------------------- | ------------ | ---------------- |
|
|
337
|
-
| `CrudPage` | CRUD 主页面 | `crud`, `data`, `permissions`, `form`, `pagination`, `density`, `detailColumns`, `showHeader`, `toolbarStyle`, `toolbarLeading`, `toolbarActions`, `card`, `tableCell`, `onrowclick`, `sortableColumns`, `nav` |
|
|
337
|
+
| `CrudPage` | CRUD 主页面 | `crud`, `data`, `permissions`, `form`, `pagination`, `density`, `detailColumns`, `showHeader`, `toolbarStyle`, `toolbarLeading`, `toolbarActions`, `card`, `tableCell`, `onrowclick`, `sortableColumns`, `nav`, `loading`, `loaded`, `error`, `onRetry`, `keepPreviousData` |
|
|
338
338
|
| `CrudFilterBar` | 过滤工具栏(搜索 + 多类型过滤 + 重置) | `filterFields`, `searchValue`, `onsearch`, `onfilterchange`, `onreset` |
|
|
339
339
|
| `CrudDetailPanel` | 详情面板(抽屉/弹窗) | `open`, `item`, `fields`, `variant`, `drawerWidth`, `modalSize` |
|
|
340
340
|
| `CrudEditPanel` | 编辑面板(抽屉/弹窗) | `open`, `fields`, `variant`, `drawerWidth`, `modalSize`, `onsubmit` |
|
|
@@ -361,6 +361,21 @@ components/
|
|
|
361
361
|
> `pagination` 配置分页栏(始终显示):`showSizeChanger`、`pageSizeOptions`、`showJumper`、`showTotal` 默认开启;紧凑视图可用 `showPageInfo` 关闭页码文案、`showFirstLast` 关闭首页 / 末页按钮。
|
|
362
362
|
>
|
|
363
363
|
> `density` 配置列表密度:`'normal'`(默认)或 `'compact'`(紧凑)。它只影响列表行、行内操作、分页与创建/编辑/详情表单元素,不影响页头与筛选栏。
|
|
364
|
+
>
|
|
365
|
+
> `CrudPage` 受控加载态:`loading` / `loaded` / `error` / `onRetry` / `keepPreviousData`。仅当 `loaded && total === 0` 时才展示空状态(避免首次请求未回时误显“暂无数据”);首次查询展示“查询中”;失败且无可用数据时展示错误与重试(`onRetry`);`keepPreviousData`(默认 true)在刷新/失败时保留旧数据。
|
|
366
|
+
>
|
|
367
|
+
> 纯客户端(无 SvelteKit SSR)可用框架级 `defineCrud` + `createCrudController` 托管 `data` 与 `nav`,自带 latest-wins(seq + AbortSignal)、失败保留旧数据、惰性加载与并发去重:
|
|
368
|
+
>
|
|
369
|
+
> ```svelte
|
|
370
|
+
> <script>
|
|
371
|
+
> import { CrudPage, createCrudController, defineCrud } from '@h-ai/ui'
|
|
372
|
+
> const crud = defineCrud({ name: 'user', label: '用户', fields: [/* ... */], api: { list: params => listUsers(params) } })
|
|
373
|
+
> const c = createCrudController(crud) // 惰性页签传 { autoLoad: false },首次激活时 c.load()
|
|
374
|
+
> </script>
|
|
375
|
+
> <CrudPage {crud} data={c.data} nav={c.nav} loading={c.loading} loaded={c.loaded} error={c.error} onRetry={c.reload} />
|
|
376
|
+
> ```
|
|
377
|
+
>
|
|
378
|
+
> `Tabs` 支持惰性页签:`lazy` 开启后未激活过的页签内容不渲染(`children` 提供 `isActivated(key)` 判断),`onactivate(key)` 在页签首次激活时触发一次,适合配合 `createCrudController({ autoLoad: false })` 在首次可见时才加载。
|
|
364
379
|
|
|
365
380
|
## 使用示例
|
|
366
381
|
|
|
@@ -382,6 +397,8 @@ components/
|
|
|
382
397
|
<ToastContainer />
|
|
383
398
|
```
|
|
384
399
|
|
|
400
|
+
> 已内置 `notifySuccess` / `notifyError` / `notifyInfo` / `notifyWarning` 封装,业务应用无需自建 toast 工具文件:`notifyError(err, fallback?)` 接受 `Error` 或字符串并内置兑底文案,默认成功 3s / 失败 5s。
|
|
401
|
+
|
|
385
402
|
### Message 消息提示
|
|
386
403
|
|
|
387
404
|
```svelte
|
|
@@ -51,23 +51,28 @@
|
|
|
51
51
|
: 'w-[2.5rem] shrink-0 [&>.fieldset]:m-0 [&>.fieldset]:min-w-0',
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
+
let jumperValue = $state(String(page))
|
|
55
|
+
|
|
54
56
|
function goToPage(p: number) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const clamped = Math.max(1, Math.min(totalPages, Math.round(p)))
|
|
58
|
+
if (clamped !== page) {
|
|
59
|
+
page = clamped
|
|
60
|
+
jumperValue = String(clamped)
|
|
61
|
+
onchange?.(clamped)
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
let jumperValue = $state('')
|
|
62
|
-
|
|
63
65
|
function handleJump() {
|
|
64
66
|
const p = Number.parseInt(jumperValue, 10)
|
|
65
67
|
if (!Number.isNaN(p)) {
|
|
66
68
|
goToPage(p)
|
|
67
|
-
jumperValue = ''
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function handleJumperBlur() {
|
|
73
|
+
handleJump()
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
function handlePageSizeChange(value: string) {
|
|
72
77
|
const next = Number.parseInt(value, 10)
|
|
73
78
|
if (!Number.isNaN(next) && next !== pageSize) {
|
|
@@ -150,6 +155,7 @@
|
|
|
150
155
|
max={totalPages}
|
|
151
156
|
bind:value={jumperValue}
|
|
152
157
|
onkeydown={(e: KeyboardEvent & { currentTarget: HTMLInputElement }) => e.key === 'Enter' && handleJump()}
|
|
158
|
+
onblur={handleJumperBlur}
|
|
153
159
|
/>
|
|
154
160
|
</div>
|
|
155
161
|
<span class={tableTextClass}>{labels.page ?? uiM('pagination_page')}</span>
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
-->
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { DataAttributes, PopoverProps } from '../../types.js'
|
|
12
|
-
import {
|
|
12
|
+
import { tick } from 'svelte'
|
|
13
|
+
import { cn, getDataAttributes, portal as portalAction } from '../../utils.js'
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
open = $bindable(false),
|
|
16
17
|
position = 'bottom',
|
|
17
18
|
trigger = 'click',
|
|
18
19
|
offset = 8,
|
|
20
|
+
portal = false,
|
|
19
21
|
class: className = '',
|
|
20
22
|
onopen,
|
|
21
23
|
onclose,
|
|
@@ -27,6 +29,10 @@
|
|
|
27
29
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
28
30
|
let triggerElement = $state<HTMLDivElement | null>(null)
|
|
29
31
|
let popoverElement = $state<HTMLDivElement | null>(null)
|
|
32
|
+
/** portal 模式下的 fixed 坐标;每次滚动或缩放后重新计算。 */
|
|
33
|
+
let portalStyle = $state('')
|
|
34
|
+
/** 首帧布局收敛后的二次定位帧,关闭时必须取消避免过期写入。 */
|
|
35
|
+
let portalPositionFrame: number | undefined = $state()
|
|
30
36
|
|
|
31
37
|
const positionClass = $derived({
|
|
32
38
|
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
|
@@ -42,6 +48,51 @@
|
|
|
42
48
|
right: 'right-full top-1/2 -translate-y-1/2 border-r-base-100 border-t-transparent border-l-transparent border-b-transparent',
|
|
43
49
|
}[position])
|
|
44
50
|
|
|
51
|
+
/** portal 模式下不再依赖相对容器,改用视口坐标并在边缘自动收敛。 */
|
|
52
|
+
function updatePortalPosition(): void {
|
|
53
|
+
if (!triggerElement || !popoverElement) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const triggerRect = triggerElement.getBoundingClientRect()
|
|
58
|
+
const popoverRect = popoverElement.getBoundingClientRect()
|
|
59
|
+
const viewportPadding = 8
|
|
60
|
+
const gap = offset
|
|
61
|
+
const maxLeft = Math.max(viewportPadding, window.innerWidth - popoverRect.width - viewportPadding)
|
|
62
|
+
const maxTop = Math.max(viewportPadding, window.innerHeight - popoverRect.height - viewportPadding)
|
|
63
|
+
let left = triggerRect.left + (triggerRect.width - popoverRect.width) / 2
|
|
64
|
+
let top = triggerRect.bottom + gap
|
|
65
|
+
|
|
66
|
+
if (position === 'top') {
|
|
67
|
+
top = triggerRect.top - popoverRect.height - gap
|
|
68
|
+
}
|
|
69
|
+
else if (position === 'left') {
|
|
70
|
+
left = triggerRect.left - popoverRect.width - gap
|
|
71
|
+
top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2
|
|
72
|
+
}
|
|
73
|
+
else if (position === 'right') {
|
|
74
|
+
left = triggerRect.right + gap
|
|
75
|
+
top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
portalStyle = `position:fixed;left:${Math.round(Math.min(Math.max(viewportPadding, left), maxLeft))}px;top:${Math.round(Math.min(Math.max(viewportPadding, top), maxTop))}px;z-index:1200;`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 等 portal 挂载完成后复测,避免首帧按未完成内容高度定位。 */
|
|
82
|
+
async function scheduleSettledPortalPosition(): Promise<void> {
|
|
83
|
+
await tick()
|
|
84
|
+
if (!open || !portal) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
portalPositionFrame = window.requestAnimationFrame(() => {
|
|
89
|
+
portalPositionFrame = undefined
|
|
90
|
+
if (open && portal) {
|
|
91
|
+
updatePortalPosition()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
45
96
|
function handleOpen() {
|
|
46
97
|
open = true
|
|
47
98
|
onopen?.()
|
|
@@ -95,6 +146,27 @@
|
|
|
95
146
|
document.removeEventListener('click', handleClickOutside)
|
|
96
147
|
}
|
|
97
148
|
})
|
|
149
|
+
|
|
150
|
+
// portal 内容脱离原始相对定位容器后,以 fixed 坐标跟随触发器滚动和缩放。
|
|
151
|
+
$effect(() => {
|
|
152
|
+
if (!open || !portal) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
updatePortalPosition()
|
|
157
|
+
void scheduleSettledPortalPosition()
|
|
158
|
+
const listenerOptions = { passive: true, capture: true } as const
|
|
159
|
+
window.addEventListener('scroll', updatePortalPosition, listenerOptions)
|
|
160
|
+
window.addEventListener('resize', updatePortalPosition)
|
|
161
|
+
return () => {
|
|
162
|
+
if (portalPositionFrame !== undefined) {
|
|
163
|
+
window.cancelAnimationFrame(portalPositionFrame)
|
|
164
|
+
portalPositionFrame = undefined
|
|
165
|
+
}
|
|
166
|
+
window.removeEventListener('scroll', updatePortalPosition, listenerOptions)
|
|
167
|
+
window.removeEventListener('resize', updatePortalPosition)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
98
170
|
</script>
|
|
99
171
|
|
|
100
172
|
<div {...dataAttributes}
|
|
@@ -117,23 +189,37 @@
|
|
|
117
189
|
{/if}
|
|
118
190
|
</div>
|
|
119
191
|
|
|
120
|
-
<!--
|
|
192
|
+
<!-- 弹出内容;portal 模式用于逃逸滚动容器的 overflow 裁剪。 -->
|
|
121
193
|
{#if open}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
194
|
+
{#if portal}
|
|
195
|
+
<div
|
|
196
|
+
bind:this={popoverElement}
|
|
197
|
+
use:portalAction
|
|
198
|
+
class='fixed z-50 rounded-box border border-base-200 bg-base-100 p-4 shadow-lg'
|
|
199
|
+
style={portalStyle}
|
|
200
|
+
role='dialog'
|
|
201
|
+
>
|
|
202
|
+
{#if children}
|
|
203
|
+
{@render children()}
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{:else}
|
|
207
|
+
<div
|
|
208
|
+
bind:this={popoverElement}
|
|
209
|
+
class={cn(
|
|
210
|
+
'absolute z-50 bg-base-100 rounded-box shadow-lg border border-base-200 p-4',
|
|
211
|
+
positionClass,
|
|
212
|
+
)}
|
|
213
|
+
style='--offset: {offset}px'
|
|
214
|
+
role='dialog'
|
|
215
|
+
>
|
|
216
|
+
<!-- 箭头 -->
|
|
217
|
+
<div class={cn('absolute w-0 h-0 border-8', arrowClass)}></div>
|
|
218
|
+
|
|
219
|
+
{#if children}
|
|
220
|
+
{@render children()}
|
|
221
|
+
{/if}
|
|
222
|
+
</div>
|
|
223
|
+
{/if}
|
|
138
224
|
{/if}
|
|
139
225
|
</div>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
<script lang='ts'>
|
|
6
6
|
import type { DataAttributes, TabsProps } from '../../types.js'
|
|
7
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
7
8
|
import { cn, getDataAttributes } from '../../utils.js'
|
|
8
9
|
import BareButton from '../primitives/BareButton.svelte'
|
|
9
10
|
|
|
@@ -14,11 +15,16 @@
|
|
|
14
15
|
type = 'line',
|
|
15
16
|
class: className = '',
|
|
16
17
|
onchange,
|
|
18
|
+
onactivate,
|
|
19
|
+
lazy = false,
|
|
17
20
|
children,
|
|
18
21
|
...restProps
|
|
19
22
|
}: TabsProps & DataAttributes = $props()
|
|
20
23
|
|
|
21
24
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
25
|
+
// 已激活过的页签集合,用于惰性挂载(不预加载不可见页签)。
|
|
26
|
+
const activatedKeys = new SvelteSet<string>()
|
|
27
|
+
|
|
22
28
|
// 默认选中第一个
|
|
23
29
|
$effect(() => {
|
|
24
30
|
if (!active && items.length > 0) {
|
|
@@ -26,6 +32,19 @@
|
|
|
26
32
|
}
|
|
27
33
|
})
|
|
28
34
|
|
|
35
|
+
// 记录激活过的页签,并在首次激活时回调一次,供惰性加载对应内容。
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (active && !activatedKeys.has(active)) {
|
|
38
|
+
activatedKeys.add(active)
|
|
39
|
+
onactivate?.(active)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** 指定页签是否已激活过;非惰性模式下恒为 true(内容全部预挂载)。 */
|
|
44
|
+
function isActivated(key: string): boolean {
|
|
45
|
+
return !lazy || activatedKeys.has(key)
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
const typeMap = {
|
|
30
49
|
line: 'tabs-border',
|
|
31
50
|
card: 'tabs-box',
|
|
@@ -77,6 +96,6 @@
|
|
|
77
96
|
|
|
78
97
|
{#if children}
|
|
79
98
|
<div class='py-4'>
|
|
80
|
-
{@render children()}
|
|
99
|
+
{@render children({ active, isActivated })}
|
|
81
100
|
</div>
|
|
82
101
|
{/if}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
=============================================================================
|
|
3
3
|
@h-ai/ui - Tooltip 组件
|
|
4
4
|
=============================================================================
|
|
5
|
-
|
|
5
|
+
提示框组件,支持两种渲染模式:
|
|
6
|
+
- 默认:DaisyUI CSS 伪元素(::before),轻量无 JS 开销
|
|
7
|
+
- portal:通过 document.body 渲染,不受父容器 overflow 裁剪
|
|
6
8
|
|
|
7
9
|
使用 Svelte 5 Runes ($props, $derived)
|
|
8
10
|
=============================================================================
|
|
@@ -14,12 +16,17 @@
|
|
|
14
16
|
const {
|
|
15
17
|
content,
|
|
16
18
|
position = 'top',
|
|
19
|
+
delay,
|
|
17
20
|
class: className = '',
|
|
18
21
|
children,
|
|
22
|
+
portal = false,
|
|
19
23
|
...restProps
|
|
20
24
|
}: TooltipProps & DataAttributes = $props()
|
|
21
25
|
|
|
22
26
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
27
|
+
|
|
28
|
+
// ─── CSS 模式(默认) ───
|
|
29
|
+
|
|
23
30
|
const positionMap = {
|
|
24
31
|
top: 'tooltip-top',
|
|
25
32
|
right: 'tooltip-right',
|
|
@@ -34,10 +41,124 @@
|
|
|
34
41
|
className,
|
|
35
42
|
),
|
|
36
43
|
)
|
|
44
|
+
|
|
45
|
+
// ─── Portal 模式 ───
|
|
46
|
+
|
|
47
|
+
let triggerEl = $state<HTMLDivElement>()
|
|
48
|
+
let portalEl: HTMLDivElement | null = null
|
|
49
|
+
|
|
50
|
+
function getClass(opacity: string): string {
|
|
51
|
+
return cn(
|
|
52
|
+
'pointer-events-none fixed z-[9999] rounded-md bg-base-content px-2 py-1 text-xs text-base-100 shadow-md',
|
|
53
|
+
className,
|
|
54
|
+
opacity,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensurePortal(): HTMLDivElement {
|
|
59
|
+
if (!portalEl && typeof document !== 'undefined') {
|
|
60
|
+
portalEl = document.createElement('div')
|
|
61
|
+
portalEl.setAttribute('role', 'tooltip')
|
|
62
|
+
portalEl.className = getClass('opacity-0')
|
|
63
|
+
document.body.appendChild(portalEl)
|
|
64
|
+
}
|
|
65
|
+
return portalEl!
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function updatePosition(): string {
|
|
69
|
+
if (!triggerEl)
|
|
70
|
+
return ''
|
|
71
|
+
|
|
72
|
+
const rect = triggerEl.getBoundingClientRect()
|
|
73
|
+
const gap = 6
|
|
74
|
+
let top: number
|
|
75
|
+
let left: number
|
|
76
|
+
let transform: string
|
|
77
|
+
|
|
78
|
+
switch (position) {
|
|
79
|
+
case 'top':
|
|
80
|
+
top = rect.top - gap
|
|
81
|
+
left = rect.left + rect.width / 2
|
|
82
|
+
transform = 'translate(-50%,-100%)'
|
|
83
|
+
break
|
|
84
|
+
case 'bottom':
|
|
85
|
+
top = rect.bottom + gap
|
|
86
|
+
left = rect.left + rect.width / 2
|
|
87
|
+
transform = 'translateX(-50%)'
|
|
88
|
+
break
|
|
89
|
+
case 'left':
|
|
90
|
+
top = rect.top + rect.height / 2
|
|
91
|
+
left = rect.left - gap
|
|
92
|
+
transform = 'translate(-100%,-50%)'
|
|
93
|
+
break
|
|
94
|
+
case 'right':
|
|
95
|
+
top = rect.top + rect.height / 2
|
|
96
|
+
left = rect.right + gap
|
|
97
|
+
transform = 'translateY(-50%)'
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `top:${top}px;left:${left}px;transform:${transform};`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let showTimer: ReturnType<typeof setTimeout> | undefined
|
|
105
|
+
|
|
106
|
+
function show() {
|
|
107
|
+
if (delay) {
|
|
108
|
+
showTimer = setTimeout(doShow, delay)
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
doShow()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function doShow() {
|
|
116
|
+
const el = ensurePortal()
|
|
117
|
+
el.className = getClass('opacity-100')
|
|
118
|
+
el.style.cssText = updatePosition()
|
|
119
|
+
el.textContent = content
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hide() {
|
|
123
|
+
if (showTimer) {
|
|
124
|
+
clearTimeout(showTimer)
|
|
125
|
+
showTimer = undefined
|
|
126
|
+
}
|
|
127
|
+
if (portalEl) {
|
|
128
|
+
portalEl.className = getClass('opacity-0')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$effect(() => {
|
|
133
|
+
return () => {
|
|
134
|
+
if (showTimer)
|
|
135
|
+
clearTimeout(showTimer)
|
|
136
|
+
if (portalEl?.parentNode) {
|
|
137
|
+
portalEl.parentNode.removeChild(portalEl)
|
|
138
|
+
portalEl = null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
})
|
|
37
142
|
</script>
|
|
38
143
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
144
|
+
{#if portal}
|
|
145
|
+
<div
|
|
146
|
+
{...dataAttributes}
|
|
147
|
+
bind:this={triggerEl}
|
|
148
|
+
class='inline-flex'
|
|
149
|
+
onmouseenter={show}
|
|
150
|
+
onmouseleave={hide}
|
|
151
|
+
onfocusin={show}
|
|
152
|
+
onfocusout={hide}
|
|
153
|
+
>
|
|
154
|
+
{#if children}
|
|
155
|
+
{@render children()}
|
|
156
|
+
{/if}
|
|
157
|
+
</div>
|
|
158
|
+
{:else}
|
|
159
|
+
<div {...dataAttributes} class={tooltipClass} data-tip={content}>
|
|
160
|
+
{#if children}
|
|
161
|
+
{@render children()}
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|