ps99-api 2.6.4 → 2.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/example-web/react2/src/components/CollectionConfigIndex.tsx +48 -2
- package/example-web/react2/src/components/ReactWindowMock.tsx +18 -4
- package/example-web/react2/src/hooks/useCollapsibleHeader.ts +13 -2
- package/example-web/react2/src/hooks/usePullToRefresh.ts +87 -0
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { FixedSizeGrid, FixedSizeList } from "./ReactWindowMock";
|
|
|
13
13
|
import AutoSizer from "./AutoSizer";
|
|
14
14
|
import { useScrollPersistence } from "../context/ScrollContext";
|
|
15
15
|
import { useCollapsibleHeader } from '../hooks/useCollapsibleHeader';
|
|
16
|
+
import { usePullToRefresh } from '../hooks/usePullToRefresh';
|
|
16
17
|
import { formatGigantix } from "../utils/gigantix";
|
|
17
18
|
|
|
18
19
|
// const FixedSizeGrid = Grid;
|
|
@@ -491,6 +492,20 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
491
492
|
disabled: isShortContent // Disable collapsing if content is short
|
|
492
493
|
});
|
|
493
494
|
|
|
495
|
+
// Pull To Refresh Logic
|
|
496
|
+
const { isRefreshing, pullDistance, onTouchStart, onTouchMove, onTouchEnd, updateScrollTop } = usePullToRefresh({
|
|
497
|
+
onRefresh: async () => {
|
|
498
|
+
// Simple reload to fetch new data
|
|
499
|
+
window.location.reload();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Update scroll top for PTR
|
|
504
|
+
const onScroll = (scrollInfo: { scrollOffset?: number, scrollTop?: number, scrollHeight?: number, clientHeight?: number }) => {
|
|
505
|
+
handleScroll(scrollInfo);
|
|
506
|
+
updateScrollTop(scrollInfo.scrollTop ?? scrollInfo.scrollOffset ?? 0);
|
|
507
|
+
};
|
|
508
|
+
|
|
494
509
|
// Loading State
|
|
495
510
|
if (loading) {
|
|
496
511
|
return (
|
|
@@ -656,6 +671,31 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
656
671
|
position: 'relative' // Context for absolute header
|
|
657
672
|
}}
|
|
658
673
|
>
|
|
674
|
+
{/* Pull To Refresh Indicator */}
|
|
675
|
+
{(pullDistance > 0 || isRefreshing) && (
|
|
676
|
+
<div style={{
|
|
677
|
+
position: 'absolute',
|
|
678
|
+
top: showHeader ? headerHeight + 5 : 0, // Adjust based on header
|
|
679
|
+
left: 0,
|
|
680
|
+
right: 0,
|
|
681
|
+
height: isRefreshing ? 60 : pullDistance,
|
|
682
|
+
display: 'flex',
|
|
683
|
+
alignItems: 'center',
|
|
684
|
+
justifyContent: 'center',
|
|
685
|
+
overflow: 'hidden',
|
|
686
|
+
backgroundColor: '#f5f5f5',
|
|
687
|
+
zIndex: 5,
|
|
688
|
+
transition: isDragging.current ? 'none' : 'height 0.3s ease'
|
|
689
|
+
}}>
|
|
690
|
+
{isRefreshing ? (
|
|
691
|
+
<div className="spinner" style={{ width: 24, height: 24, border: '3px solid #ccc', borderTopColor: '#333', borderRadius: '50%' }}></div>
|
|
692
|
+
) : (
|
|
693
|
+
<span style={{ opacity: Math.min(pullDistance / 60, 1), transform: `rotate(${pullDistance * 2}deg)` }}>
|
|
694
|
+
⬇️
|
|
695
|
+
</span>
|
|
696
|
+
)}
|
|
697
|
+
</div>
|
|
698
|
+
)}
|
|
659
699
|
{/* Header Bar inside Window */}
|
|
660
700
|
<div
|
|
661
701
|
ref={headerRef}
|
|
@@ -828,8 +868,11 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
828
868
|
itemSize={80} // List view row height
|
|
829
869
|
width={width}
|
|
830
870
|
initialScrollOffset={initialScrollOffset}
|
|
831
|
-
onScroll={
|
|
871
|
+
onScroll={onScroll} // Use wrapped handler
|
|
832
872
|
bottomPadding={120} // Account for Android footer/nav
|
|
873
|
+
onTouchStart={onTouchStart}
|
|
874
|
+
onTouchMove={onTouchMove}
|
|
875
|
+
onTouchEnd={onTouchEnd}
|
|
833
876
|
itemData={{
|
|
834
877
|
items: finalItems,
|
|
835
878
|
navigate,
|
|
@@ -861,9 +904,12 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
861
904
|
rowHeight={220}
|
|
862
905
|
width={width}
|
|
863
906
|
initialScrollOffset={initialScrollOffset}
|
|
864
|
-
onScroll={
|
|
907
|
+
onScroll={onScroll} // Use wrapped handler
|
|
865
908
|
style={{ overflowX: "hidden" }}
|
|
866
909
|
bottomPadding={120} // Account for Android footer/nav
|
|
910
|
+
onTouchStart={onTouchStart}
|
|
911
|
+
onTouchMove={onTouchMove}
|
|
912
|
+
onTouchEnd={onTouchEnd}
|
|
867
913
|
itemData={{
|
|
868
914
|
items: finalItems,
|
|
869
915
|
columnCount: colCount,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
export const FixedSizeList = ({ children, itemCount, itemSize, height, width, onScroll, initialScrollOffset, itemData, bottomPadding = 0 }: any) => {
|
|
3
|
+
export const FixedSizeList = ({ children, itemCount, itemSize, height, width, onScroll, initialScrollOffset, itemData, bottomPadding = 0, onTouchStart, onTouchMove, onTouchEnd }: any) => {
|
|
4
4
|
const items = [];
|
|
5
5
|
for (let i = 0; i < itemCount; i++) {
|
|
6
6
|
items.push(
|
|
@@ -22,7 +22,14 @@ export const FixedSizeList = ({ children, itemCount, itemSize, height, width, on
|
|
|
22
22
|
<div
|
|
23
23
|
ref={ref}
|
|
24
24
|
style={{ height, width, overflow: "auto", position: 'relative' }}
|
|
25
|
-
onScroll={(e) => onScroll && onScroll({
|
|
25
|
+
onScroll={(e) => onScroll && onScroll({
|
|
26
|
+
scrollOffset: e.currentTarget.scrollTop,
|
|
27
|
+
scrollHeight: e.currentTarget.scrollHeight,
|
|
28
|
+
clientHeight: e.currentTarget.clientHeight
|
|
29
|
+
})}
|
|
30
|
+
onTouchStart={onTouchStart}
|
|
31
|
+
onTouchMove={onTouchMove}
|
|
32
|
+
onTouchEnd={onTouchEnd}
|
|
26
33
|
>
|
|
27
34
|
{items}
|
|
28
35
|
{bottomPadding > 0 && <div style={{ height: bottomPadding }} />}
|
|
@@ -30,7 +37,7 @@ export const FixedSizeList = ({ children, itemCount, itemSize, height, width, on
|
|
|
30
37
|
);
|
|
31
38
|
};
|
|
32
39
|
|
|
33
|
-
export const FixedSizeGrid = ({ children, columnCount, rowCount, columnWidth, rowHeight, height, width, onScroll, initialScrollOffset, itemData, style, bottomPadding = 0 }: any) => {
|
|
40
|
+
export const FixedSizeGrid = ({ children, columnCount, rowCount, columnWidth, rowHeight, height, width, onScroll, initialScrollOffset, itemData, style, bottomPadding = 0, onTouchStart, onTouchMove, onTouchEnd }: any) => {
|
|
34
41
|
const items = [];
|
|
35
42
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
36
43
|
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
|
|
@@ -64,7 +71,14 @@ export const FixedSizeGrid = ({ children, columnCount, rowCount, columnWidth, ro
|
|
|
64
71
|
<div
|
|
65
72
|
ref={ref}
|
|
66
73
|
style={{ height, width, overflowY: "auto", overflowX: "hidden", position: 'relative', ...style }}
|
|
67
|
-
onScroll={(e) => onScroll && onScroll({
|
|
74
|
+
onScroll={(e) => onScroll && onScroll({
|
|
75
|
+
scrollTop: e.currentTarget.scrollTop,
|
|
76
|
+
scrollHeight: e.currentTarget.scrollHeight,
|
|
77
|
+
clientHeight: e.currentTarget.clientHeight
|
|
78
|
+
})}
|
|
79
|
+
onTouchStart={onTouchStart}
|
|
80
|
+
onTouchMove={onTouchMove}
|
|
81
|
+
onTouchEnd={onTouchEnd}
|
|
68
82
|
>
|
|
69
83
|
<div style={{ height: rowCount * rowHeight + bottomPadding, width: columnWidth * columnCount }}>
|
|
70
84
|
{items}
|
|
@@ -16,7 +16,7 @@ export const useCollapsibleHeader = (options: UseCollapsibleHeaderOptions = {})
|
|
|
16
16
|
const lastScrollTop = useRef(0);
|
|
17
17
|
const scrollRef = useRef<number>(0);
|
|
18
18
|
|
|
19
|
-
const handleScroll = ({ scrollOffset, scrollTop }: { scrollOffset?: number, scrollTop?: number }) => {
|
|
19
|
+
const handleScroll = ({ scrollOffset, scrollTop, scrollHeight, clientHeight }: { scrollOffset?: number, scrollTop?: number, scrollHeight?: number, clientHeight?: number }) => {
|
|
20
20
|
if (disabled) {
|
|
21
21
|
setShowHeader(true);
|
|
22
22
|
return;
|
|
@@ -25,6 +25,14 @@ export const useCollapsibleHeader = (options: UseCollapsibleHeaderOptions = {})
|
|
|
25
25
|
const currentScrollTop = scrollTop ?? scrollOffset ?? 0;
|
|
26
26
|
const scrollDelta = currentScrollTop - lastScrollTop.current;
|
|
27
27
|
|
|
28
|
+
// Bounce Protection: Detect if we are near the bottom
|
|
29
|
+
// If we are at the bottom, a "scroll up" might be a bounce.
|
|
30
|
+
let isNearBottom = false;
|
|
31
|
+
if (scrollHeight && clientHeight) {
|
|
32
|
+
// Buffer of 100px to be safe
|
|
33
|
+
isNearBottom = (currentScrollTop + clientHeight) >= (scrollHeight - 100);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
// Threshold to prevent jitter
|
|
29
37
|
if (Math.abs(scrollDelta) > threshold) {
|
|
30
38
|
if (scrollDelta > 0 && currentScrollTop > triggerStart) {
|
|
@@ -32,7 +40,10 @@ export const useCollapsibleHeader = (options: UseCollapsibleHeaderOptions = {})
|
|
|
32
40
|
setShowHeader(false);
|
|
33
41
|
} else if (scrollDelta < 0) {
|
|
34
42
|
// Scrolling Up
|
|
35
|
-
|
|
43
|
+
// ONLY show header if we are NOT at the very bottom (prevent bounce triggering it)
|
|
44
|
+
if (!isNearBottom) {
|
|
45
|
+
setShowHeader(true);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
lastScrollTop.current = currentScrollTop;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface PullToRefreshOptions {
|
|
4
|
+
onRefresh: () => Promise<void> | void;
|
|
5
|
+
threshold?: number; // px to pull down to trigger
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const usePullToRefresh = ({ onRefresh, threshold = 80 }: PullToRefreshOptions) => {
|
|
9
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
10
|
+
const [pullDistance, setPullDistance] = useState(0);
|
|
11
|
+
const startY = useRef<number>(0);
|
|
12
|
+
const isDragging = useRef(false);
|
|
13
|
+
|
|
14
|
+
// We need to know the scroll position to only allow pull when at top
|
|
15
|
+
const scrollTopRef = useRef(0);
|
|
16
|
+
|
|
17
|
+
const onTouchStart = (e: React.TouchEvent) => {
|
|
18
|
+
if (scrollTopRef.current === 0) {
|
|
19
|
+
startY.current = e.touches[0].clientY;
|
|
20
|
+
isDragging.current = true;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const onTouchMove = (e: React.TouchEvent) => {
|
|
25
|
+
if (!isDragging.current) return;
|
|
26
|
+
if (scrollTopRef.current > 0) {
|
|
27
|
+
isDragging.current = false;
|
|
28
|
+
setPullDistance(0);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const currentY = e.touches[0].clientY;
|
|
33
|
+
const diff = currentY - startY.current;
|
|
34
|
+
|
|
35
|
+
if (diff > 0) {
|
|
36
|
+
// Resistance effect
|
|
37
|
+
setPullDistance(Math.min(diff * 0.5, threshold * 1.5));
|
|
38
|
+
// Prevent native scroll if we are pulling down?
|
|
39
|
+
// Might interfere with normal scrolling if not careful.
|
|
40
|
+
// But since scrollTop is 0, pulling down usually does nothing in overflow:auto unless overscroll-behavior is set.
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const onTouchEnd = async () => {
|
|
45
|
+
if (!isDragging.current) return;
|
|
46
|
+
isDragging.current = false;
|
|
47
|
+
|
|
48
|
+
if (pullDistance > threshold) {
|
|
49
|
+
setIsRefreshing(true);
|
|
50
|
+
setPullDistance(threshold); // Snap to threshold
|
|
51
|
+
try {
|
|
52
|
+
await onRefresh();
|
|
53
|
+
} finally {
|
|
54
|
+
setIsRefreshing(false);
|
|
55
|
+
setPullDistance(0);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
setPullDistance(0);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const updateScrollTop = (val: number) => {
|
|
63
|
+
scrollTopRef.current = val;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Expose boolean for UI
|
|
67
|
+
const [isDraggingState, setIsDraggingState] = useState(false);
|
|
68
|
+
|
|
69
|
+
// Sync ref to state for UI (optional, or just use state)
|
|
70
|
+
// Actually, let's just use state for the critical UI part if needed,
|
|
71
|
+
// but typically we want ref for perf on touchmove.
|
|
72
|
+
// Let's just return a getter or similar?
|
|
73
|
+
// Simplest: just don't use isDragging for the transition logic in the UI, or use pullDistance === 0 check.
|
|
74
|
+
|
|
75
|
+
// Correction: In proper PTR, you want no transition during drag, but transition on release.
|
|
76
|
+
// So we need to know if we are dragging.
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
isRefreshing,
|
|
80
|
+
pullDistance,
|
|
81
|
+
onTouchStart: (e: React.TouchEvent) => { onTouchStart(e); setIsDraggingState(true); },
|
|
82
|
+
onTouchMove,
|
|
83
|
+
onTouchEnd: async () => { setIsDraggingState(false); await onTouchEnd(); },
|
|
84
|
+
updateScrollTop,
|
|
85
|
+
isDragging: isDraggingState
|
|
86
|
+
};
|
|
87
|
+
};
|