nexa-ui-kit 0.6.2 → 0.7.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/dist/components/NCard.js +5 -1
- package/dist/components/NDataTable.js +50 -38
- package/dist/components/NDataTable.nexa +19 -2
- package/dist/components/NImage.js +104 -0
- package/dist/components/NImage.nexa +112 -0
- package/dist/components/NModal.js +31 -25
- package/dist/components/NModal.nexa +5 -3
- package/dist/components/NScrollView.js +128 -0
- package/dist/components/NScrollView.nexa +136 -0
- package/dist/components/NTabs.js +1 -1
- package/dist/components/NVirtualList.js +91 -0
- package/dist/components/NVirtualList.nexa +94 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +4 -4
- package/src/components/NDataTable.nexa +19 -2
- package/src/components/NImage.nexa +112 -0
- package/src/components/NModal.nexa +5 -3
- package/src/components/NScrollView.nexa +136 -0
- package/src/components/NVirtualList.nexa +94 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { signal, computed } from 'nexa-framework'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
onRefresh: { type: Function, default: null },
|
|
6
|
+
onEndReached: { type: Function, default: null },
|
|
7
|
+
endReachedThreshold: { type: Number, default: 80 },
|
|
8
|
+
horizontal: { type: Boolean, default: false }
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const isRefreshing = signal(false)
|
|
12
|
+
const pullDelta = signal(0)
|
|
13
|
+
const refreshThreshold = 60
|
|
14
|
+
let touchStartY = 0
|
|
15
|
+
let startScrollTop = 0
|
|
16
|
+
|
|
17
|
+
const pullProgress = computed(() =>
|
|
18
|
+
Math.min(1, pullDelta.value / refreshThreshold)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const spinnerScale = computed(() =>
|
|
22
|
+
`scale(${pullProgress.value})`
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const onTouchStart = (e) => {
|
|
26
|
+
const container = e.currentTarget
|
|
27
|
+
startScrollTop = container.scrollTop
|
|
28
|
+
touchStartY = e.touches[0].clientY
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const onTouchMove = (e) => {
|
|
32
|
+
if (isRefreshing.value || !props.onRefresh) return
|
|
33
|
+
const container = e.currentTarget
|
|
34
|
+
const dy = e.touches[0].clientY - touchStartY
|
|
35
|
+
if (container.scrollTop === 0 && dy > 0 && startScrollTop === 0) {
|
|
36
|
+
pullDelta.value = Math.min(dy * 0.5, refreshThreshold * 1.5)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onTouchEnd = async () => {
|
|
41
|
+
if (!props.onRefresh || pullDelta.value < refreshThreshold) {
|
|
42
|
+
pullDelta.value = 0
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
isRefreshing.value = true
|
|
46
|
+
pullDelta.value = 0
|
|
47
|
+
try {
|
|
48
|
+
await props.onRefresh()
|
|
49
|
+
} finally {
|
|
50
|
+
isRefreshing.value = false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const onScroll = (e) => {
|
|
55
|
+
if (!props.onEndReached) return
|
|
56
|
+
const el = e.target
|
|
57
|
+
const distanceFromBottom = props.horizontal
|
|
58
|
+
? el.scrollWidth - el.scrollLeft - el.clientWidth
|
|
59
|
+
: el.scrollHeight - el.scrollTop - el.clientHeight
|
|
60
|
+
if (distanceFromBottom <= props.endReachedThreshold) {
|
|
61
|
+
props.onEndReached()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div
|
|
68
|
+
class="n-scroll-view"
|
|
69
|
+
:class="{ 'n-scroll-view--horizontal': horizontal }"
|
|
70
|
+
@scroll="onScroll"
|
|
71
|
+
@touchstart="onTouchStart"
|
|
72
|
+
@touchmove="onTouchMove"
|
|
73
|
+
@touchend="onTouchEnd"
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
v-if="onRefresh"
|
|
77
|
+
class="n-scroll-view-ptr"
|
|
78
|
+
:class="{ 'is-refreshing': isRefreshing.value }"
|
|
79
|
+
:style="{ height: pullDelta.value + 'px' }"
|
|
80
|
+
>
|
|
81
|
+
<div
|
|
82
|
+
class="n-scroll-view-spinner"
|
|
83
|
+
:style="{ transform: isRefreshing.value ? 'scale(1)' : spinnerScale.value }"
|
|
84
|
+
>
|
|
85
|
+
⟳
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<slot />
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<style scoped>
|
|
93
|
+
.n-scroll-view {
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
overflow-x: hidden;
|
|
96
|
+
width: 100%;
|
|
97
|
+
height: 100%;
|
|
98
|
+
-webkit-overflow-scrolling: touch;
|
|
99
|
+
overscroll-behavior: contain;
|
|
100
|
+
position: relative;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.n-scroll-view--horizontal {
|
|
104
|
+
overflow-y: hidden;
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
display: flex;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.n-scroll-view-ptr {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
transition: height 0.1s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.n-scroll-view-ptr.is-refreshing {
|
|
118
|
+
height: 52px !important;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.n-scroll-view-spinner {
|
|
122
|
+
font-size: 24px;
|
|
123
|
+
color: var(--n-color-primary, #6366f1);
|
|
124
|
+
transition: transform 0.1s;
|
|
125
|
+
line-height: 1;
|
|
126
|
+
transform-origin: center;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.n-scroll-view-ptr.is-refreshing .n-scroll-view-spinner {
|
|
130
|
+
animation: n-spin 0.8s linear infinite;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@keyframes n-spin {
|
|
134
|
+
to { transform: rotate(360deg) scale(1); }
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { signal, computed, onMounted, onUnmounted, effect } from 'nexa-framework'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
items: { type: Array, default: () => [] },
|
|
6
|
+
itemHeight: { type: Number, default: 48 },
|
|
7
|
+
overscan: { type: Number, default: 3 },
|
|
8
|
+
renderItem: { type: Function, required: true },
|
|
9
|
+
keyFn: { type: Function, default: null }
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const containerRef = signal(null)
|
|
13
|
+
const scrollTop = signal(0)
|
|
14
|
+
const containerHeight = signal(400)
|
|
15
|
+
|
|
16
|
+
const totalHeight = computed(() => props.items.length * props.itemHeight)
|
|
17
|
+
|
|
18
|
+
const visibleRange = computed(() => {
|
|
19
|
+
const start = Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.overscan)
|
|
20
|
+
const visibleCount = Math.ceil(containerHeight.value / props.itemHeight) + props.overscan * 2
|
|
21
|
+
const end = Math.min(props.items.length, start + visibleCount)
|
|
22
|
+
return { start, end }
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const visibleItems = computed(() => {
|
|
26
|
+
const { start, end } = visibleRange.value
|
|
27
|
+
return props.items.slice(start, end).map((item, i) => ({
|
|
28
|
+
item,
|
|
29
|
+
index: start + i,
|
|
30
|
+
top: (start + i) * props.itemHeight
|
|
31
|
+
}))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
let resizeObserver = null
|
|
35
|
+
|
|
36
|
+
onMounted(() => {
|
|
37
|
+
const el = containerRef.value
|
|
38
|
+
if (!el) return
|
|
39
|
+
containerHeight.value = el.clientHeight
|
|
40
|
+
|
|
41
|
+
resizeObserver = new ResizeObserver(entries => {
|
|
42
|
+
containerHeight.value = entries[0].contentRect.height
|
|
43
|
+
})
|
|
44
|
+
resizeObserver.observe(el)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
onUnmounted(() => {
|
|
48
|
+
resizeObserver?.disconnect()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const onScroll = (e) => {
|
|
52
|
+
scrollTop.value = e.target.scrollTop
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div
|
|
58
|
+
class="n-virtual-list"
|
|
59
|
+
@scroll="onScroll"
|
|
60
|
+
:ref="el => containerRef.value = el"
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
class="n-virtual-list-spacer"
|
|
64
|
+
:style="{ height: totalHeight.value + 'px', position: 'relative' }"
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
v-for="row in visibleItems.value"
|
|
68
|
+
:key="keyFn ? keyFn(row.item, row.index) : row.index"
|
|
69
|
+
class="n-virtual-list-row"
|
|
70
|
+
:style="{ position: 'absolute', top: row.top + 'px', width: '100%', height: itemHeight + 'px' }"
|
|
71
|
+
>
|
|
72
|
+
{{ renderItem(row.item, row.index) }}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.n-virtual-list {
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
width: 100%;
|
|
82
|
+
height: 100%;
|
|
83
|
+
-webkit-overflow-scrolling: touch;
|
|
84
|
+
overscroll-behavior: contain;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.n-virtual-list-spacer {
|
|
88
|
+
position: relative;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.n-virtual-list-row {
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,9 @@ export { default as NInputNumber } from './components/NInputNumber.nexa'
|
|
|
30
30
|
export { default as NMultiSelect } from './components/NMultiSelect.nexa'
|
|
31
31
|
export { default as NDataTable } from './components/NDataTable.nexa'
|
|
32
32
|
export { default as NPaginator } from './components/NPaginator.nexa'
|
|
33
|
+
export { default as NVirtualList } from './components/NVirtualList.nexa'
|
|
34
|
+
export { default as NScrollView } from './components/NScrollView.nexa'
|
|
35
|
+
export { default as NImage } from './components/NImage.nexa'
|
|
33
36
|
export * from './services/ToastService.js'
|
|
34
37
|
export * from './services/FormValidation.js'
|
|
35
38
|
export { installTheme } from './styles/theme.js'
|