funda-ui 4.7.115 → 4.7.133
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/CascadingSelect/index.d.ts +1 -0
- package/CascadingSelect/index.js +7 -3
- package/CascadingSelectE2E/index.d.ts +1 -0
- package/CascadingSelectE2E/index.js +7 -3
- package/Date/index.js +25 -2
- package/EventCalendar/index.js +25 -2
- package/EventCalendarTimeline/index.js +25 -2
- package/README.md +9 -11
- package/Refresher/index.d.ts +22 -0
- package/Refresher/index.js +564 -0
- package/SplitterPanel/index.css +63 -0
- package/SplitterPanel/index.d.ts +20 -0
- package/SplitterPanel/index.js +800 -0
- package/Utils/date.d.ts +15 -5
- package/Utils/date.js +22 -2
- package/Utils/time.d.ts +34 -0
- package/Utils/time.js +162 -0
- package/Utils/useIsMobile.js +66 -2
- package/all.d.ts +2 -0
- package/all.js +2 -0
- package/lib/cjs/CascadingSelect/index.d.ts +1 -0
- package/lib/cjs/CascadingSelect/index.js +7 -3
- package/lib/cjs/CascadingSelectE2E/index.d.ts +1 -0
- package/lib/cjs/CascadingSelectE2E/index.js +7 -3
- package/lib/cjs/Date/index.js +25 -2
- package/lib/cjs/EventCalendar/index.js +25 -2
- package/lib/cjs/EventCalendarTimeline/index.js +25 -2
- package/lib/cjs/Refresher/index.d.ts +22 -0
- package/lib/cjs/Refresher/index.js +564 -0
- package/lib/cjs/SplitterPanel/index.d.ts +20 -0
- package/lib/cjs/SplitterPanel/index.js +800 -0
- package/lib/cjs/Utils/date.d.ts +15 -5
- package/lib/cjs/Utils/date.js +22 -2
- package/lib/cjs/Utils/time.d.ts +34 -0
- package/lib/cjs/Utils/time.js +162 -0
- package/lib/cjs/Utils/useIsMobile.js +66 -2
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +2 -0
- package/lib/css/SplitterPanel/index.css +63 -0
- package/lib/esm/CascadingSelect/Group.tsx +4 -2
- package/lib/esm/CascadingSelect/index.tsx +3 -0
- package/lib/esm/CascadingSelectE2E/Group.tsx +4 -2
- package/lib/esm/CascadingSelectE2E/index.tsx +3 -0
- package/lib/esm/Refresher/index.tsx +121 -0
- package/lib/esm/SplitterPanel/index.scss +82 -0
- package/lib/esm/SplitterPanel/index.tsx +174 -0
- package/lib/esm/Utils/hooks/useIsMobile.tsx +90 -4
- package/lib/esm/Utils/libs/date.ts +28 -8
- package/lib/esm/Utils/libs/time.ts +125 -0
- package/lib/esm/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import useIsMobile from 'funda-utils/dist/cjs/useIsMobile';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
interface RefresherProps {
|
|
7
|
+
/** Pulling text */
|
|
8
|
+
pullingText?: React.ReactNode;
|
|
9
|
+
/** Text when reaching the threshold */
|
|
10
|
+
readyToRefreshText?: React.ReactNode;
|
|
11
|
+
/** Refreshing text */
|
|
12
|
+
refreshingText?: React.ReactNode;
|
|
13
|
+
/** The pull distance (px) that triggers the refresh */
|
|
14
|
+
threshold?: number;
|
|
15
|
+
/** The height of the trigger area */
|
|
16
|
+
triggerHeight?: number;
|
|
17
|
+
/** The styles of the trigger area */
|
|
18
|
+
triggerAreaStyle?: React.CSSProperties;
|
|
19
|
+
/** Pull-down is only allowed when the scroll bar of this element is at the top. You can only fire certain actions when the scrollbar is at the top by listening to the scroll event of a given DOM element and determining if its scrollTop is 0. */
|
|
20
|
+
scrollableElementClassName?: string;
|
|
21
|
+
/** Refresh action is async */
|
|
22
|
+
onRefresh: () => Promise<void>;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Refresher = (prpps: RefresherProps) => {
|
|
27
|
+
const {
|
|
28
|
+
onRefresh,
|
|
29
|
+
children,
|
|
30
|
+
pullingText = 'Pull down to refresh',
|
|
31
|
+
readyToRefreshText = 'Release to refresh',
|
|
32
|
+
refreshingText = 'Refreshing...',
|
|
33
|
+
threshold = 100,
|
|
34
|
+
triggerHeight = 50,
|
|
35
|
+
triggerAreaStyle,
|
|
36
|
+
scrollableElementClassName,
|
|
37
|
+
} = prpps;
|
|
38
|
+
|
|
39
|
+
const isMobile = useIsMobile();
|
|
40
|
+
const [pullDistance, setPullDistance] = useState<number>(0);
|
|
41
|
+
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
|
42
|
+
const startY = useRef<number | null>(null);
|
|
43
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
|
|
45
|
+
const handleTouchStart = (e: React.TouchEvent) => {
|
|
46
|
+
if (isRefreshing) return;
|
|
47
|
+
// Pull-down is only allowed when the scroll bar is at the top
|
|
48
|
+
const targetScrollableDiv = scrollableElementClassName === null || typeof scrollableElementClassName === 'undefined' ? containerRef.current : document.querySelector(scrollableElementClassName as string);
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if (targetScrollableDiv && targetScrollableDiv.scrollTop > 0) return;
|
|
52
|
+
startY.current = e.touches[0].clientY;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleTouchMove = (e: React.TouchEvent) => {
|
|
56
|
+
if (startY.current === null || isRefreshing) return;
|
|
57
|
+
const distance = e.touches[0].clientY - startY.current;
|
|
58
|
+
if (distance > 0) {
|
|
59
|
+
setPullDistance(distance > threshold ? threshold : distance); // The maximum pull should not exceed "threshold" value
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleTouchEnd = async () => {
|
|
64
|
+
if (isRefreshing) return;
|
|
65
|
+
if (pullDistance >= threshold) {
|
|
66
|
+
setIsRefreshing(true);
|
|
67
|
+
await onRefresh();
|
|
68
|
+
setIsRefreshing(false);
|
|
69
|
+
}
|
|
70
|
+
setPullDistance(0);
|
|
71
|
+
startY.current = null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
{!isMobile ? <React.Fragment>
|
|
77
|
+
{/** CONTENT */}
|
|
78
|
+
{children}
|
|
79
|
+
</React.Fragment> : <div
|
|
80
|
+
ref={containerRef}
|
|
81
|
+
style={{
|
|
82
|
+
overflow: 'hidden',
|
|
83
|
+
touchAction: 'pan-y',
|
|
84
|
+
marginTop: `-${triggerHeight}px`,
|
|
85
|
+
}}
|
|
86
|
+
onTouchStart={handleTouchStart}
|
|
87
|
+
onTouchMove={handleTouchMove}
|
|
88
|
+
onTouchEnd={handleTouchEnd}
|
|
89
|
+
>
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
transform: `translateY(${isRefreshing ? triggerHeight : pullDistance}px)`,
|
|
93
|
+
transition: isRefreshing ? 'transform 0.2s' : 'transform 0.3s',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
|
|
97
|
+
{/** TRIGGER */}
|
|
98
|
+
<div style={{
|
|
99
|
+
...triggerAreaStyle,
|
|
100
|
+
height: `${triggerHeight}px`,
|
|
101
|
+
textAlign: 'center',
|
|
102
|
+
lineHeight: '50px'
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{isRefreshing
|
|
106
|
+
? refreshingText
|
|
107
|
+
: pullDistance >= threshold
|
|
108
|
+
? readyToRefreshText
|
|
109
|
+
: pullingText}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/** CONTENT */}
|
|
113
|
+
{children}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
}
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default Refresher;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
/* ======================================================
|
|
3
|
+
<!-- Splitter Panel -->
|
|
4
|
+
/* ====================================================== */
|
|
5
|
+
.splitter-panel {
|
|
6
|
+
|
|
7
|
+
--splitter-panel-divider-color: rgba(0, 0, 0, 0.15);
|
|
8
|
+
--splitter-panel-divider-hover-color: rgba(210, 253, 253, .86);
|
|
9
|
+
|
|
10
|
+
display: flex;
|
|
11
|
+
position: relative;
|
|
12
|
+
|
|
13
|
+
.splitter-panel-left {
|
|
14
|
+
width: var(--splitter-panel-left-w, 0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.splitter-panel-divider {
|
|
18
|
+
position: relative;
|
|
19
|
+
width: 1px;
|
|
20
|
+
cursor: col-resize;
|
|
21
|
+
padding: 0 2px;
|
|
22
|
+
transition: all 0.25s;
|
|
23
|
+
z-index: 1;
|
|
24
|
+
|
|
25
|
+
&::before {
|
|
26
|
+
content: '';
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 0;
|
|
29
|
+
left: 0;
|
|
30
|
+
width: 1px;
|
|
31
|
+
height: 100%;
|
|
32
|
+
background: var(--splitter-panel-divider-color);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&::after {
|
|
36
|
+
content: '';
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: 0;
|
|
39
|
+
left: -2px;
|
|
40
|
+
width: 5px;
|
|
41
|
+
height: 100%;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
&:hover::after {
|
|
46
|
+
background: var(--splitter-panel-divider-hover-color);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
.splitter-panel-right {
|
|
52
|
+
flex: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
.splitter-panel-vertical {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
|
|
64
|
+
.splitter-panel-top,
|
|
65
|
+
.splitter-panel-bottom {
|
|
66
|
+
min-height: 50px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@media (max-width: 767px) {
|
|
73
|
+
.splitter-panel {
|
|
74
|
+
display: none !important;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@media (min-width: 768px) {
|
|
79
|
+
.splitter-panel-vertical {
|
|
80
|
+
display: none !important;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef, useImperativeHandle } from 'react';
|
|
2
|
+
|
|
3
|
+
import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
|
|
4
|
+
import useIsMobile from 'funda-utils/dist/cjs/useIsMobile';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
interface SplitterPanelSubProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SplitterPanelLeft: React.FC<SplitterPanelSubProps> = ({ children }) => <>{children}</>;
|
|
12
|
+
const SplitterPanelRight: React.FC<SplitterPanelSubProps> = ({ children }) => <>{children}</>;
|
|
13
|
+
|
|
14
|
+
interface SplitterPanelProps {
|
|
15
|
+
wrapperClassName?: string;
|
|
16
|
+
draggable?: boolean
|
|
17
|
+
initialWidth?: number;
|
|
18
|
+
minWidth?: number;
|
|
19
|
+
maxWidth?: number;
|
|
20
|
+
onDrag?: (type: 'dragStart' | 'dragEnd' | 'drag', leftWidth: number) => void;
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const SplitterPanel: React.FC<SplitterPanelProps> & {
|
|
26
|
+
Left: typeof SplitterPanelLeft;
|
|
27
|
+
Right: typeof SplitterPanelRight;
|
|
28
|
+
} = (props) => {
|
|
29
|
+
const {
|
|
30
|
+
wrapperClassName,
|
|
31
|
+
draggable = true,
|
|
32
|
+
maxWidth,
|
|
33
|
+
minWidth = 100,
|
|
34
|
+
initialWidth = 200,
|
|
35
|
+
onDrag,
|
|
36
|
+
children
|
|
37
|
+
} = props;
|
|
38
|
+
|
|
39
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
const dividerRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
const getDefaultWidth = () => {
|
|
42
|
+
if (typeof initialWidth === 'number') return initialWidth;
|
|
43
|
+
if (typeof minWidth === 'number') return minWidth;
|
|
44
|
+
return 200;
|
|
45
|
+
};
|
|
46
|
+
const [leftWidth, setLeftWidth] = useState<number>(getDefaultWidth());
|
|
47
|
+
const isMobile = useIsMobile();
|
|
48
|
+
|
|
49
|
+
// adjust split panel
|
|
50
|
+
const [dragging, setDragging] = useState<boolean>(false);
|
|
51
|
+
|
|
52
|
+
// Update only when initialWidth changes and is valid, and only when it has not been dragged
|
|
53
|
+
const hasDragged = useRef(false);
|
|
54
|
+
|
|
55
|
+
let left: React.ReactNode = null;
|
|
56
|
+
let right: React.ReactNode = null;
|
|
57
|
+
|
|
58
|
+
React.Children.forEach(children, (child) => {
|
|
59
|
+
if (!React.isValidElement(child)) return;
|
|
60
|
+
if (child.type === SplitterPanelLeft) left = child.props.children;
|
|
61
|
+
if (child.type === SplitterPanelRight) right = child.props.children;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// dragdrop
|
|
67
|
+
function handleSplitPanelsChange(clientX: number) {
|
|
68
|
+
if (!containerRef.current) return;
|
|
69
|
+
const containerLeft = containerRef.current.getBoundingClientRect().left;
|
|
70
|
+
const separatorXPosition = clientX - containerLeft;
|
|
71
|
+
const minWidthValue = minWidth || 100;
|
|
72
|
+
const maxWidthValue = maxWidth || window.innerWidth / 2;
|
|
73
|
+
if (dragging && separatorXPosition > minWidthValue && separatorXPosition < maxWidthValue) {
|
|
74
|
+
setLeftWidth(separatorXPosition);
|
|
75
|
+
onDrag?.('drag', separatorXPosition);
|
|
76
|
+
hasDragged.current = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function handleMouseDownSplitPanels(e: React.MouseEvent) {
|
|
80
|
+
setDragging(true);
|
|
81
|
+
onDrag?.('dragStart', leftWidth ?? initialWidth);
|
|
82
|
+
}
|
|
83
|
+
function handleTouchStartSplitPanels(e: React.TouchEvent) {
|
|
84
|
+
setDragging(true);
|
|
85
|
+
onDrag?.('dragStart', leftWidth ?? initialWidth);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleMouseMoveSplitPanels(e: MouseEvent) {
|
|
89
|
+
handleSplitPanelsChange(e.clientX);
|
|
90
|
+
}
|
|
91
|
+
function handleTouchMoveSplitPanels(e: TouchEvent) {
|
|
92
|
+
handleSplitPanelsChange(e.touches[0].clientX);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handleMouseUpSplitPanels() {
|
|
96
|
+
setDragging(false);
|
|
97
|
+
onDrag?.('dragEnd', leftWidth ?? initialWidth);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!draggable) return;
|
|
103
|
+
|
|
104
|
+
// 事件监听用原生事件
|
|
105
|
+
document.addEventListener('mousemove', handleMouseMoveSplitPanels, false);
|
|
106
|
+
document.addEventListener('touchmove', handleTouchMoveSplitPanels, false);
|
|
107
|
+
document.addEventListener('mouseup', handleMouseUpSplitPanels, false);
|
|
108
|
+
document.addEventListener('touchend', handleMouseUpSplitPanels, false);
|
|
109
|
+
return () => {
|
|
110
|
+
document.removeEventListener('mousemove', handleMouseMoveSplitPanels, false);
|
|
111
|
+
document.removeEventListener('touchmove', handleTouchMoveSplitPanels, false);
|
|
112
|
+
document.removeEventListener('mouseup', handleMouseUpSplitPanels, false);
|
|
113
|
+
document.removeEventListener('touchend', handleMouseUpSplitPanels, false);
|
|
114
|
+
};
|
|
115
|
+
}, [draggable, dragging]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!hasDragged.current && typeof initialWidth === 'number') {
|
|
119
|
+
setLeftWidth(initialWidth);
|
|
120
|
+
}
|
|
121
|
+
}, [initialWidth]);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
|
|
126
|
+
{isMobile ? <>
|
|
127
|
+
<div className={combinedCls(
|
|
128
|
+
'splitter-panel-vertical',
|
|
129
|
+
clsWrite(wrapperClassName, '')
|
|
130
|
+
)}>
|
|
131
|
+
<div className="splitter-panel-top">{left}</div>
|
|
132
|
+
<div className="splitter-panel-bottom">{right}</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
</> : <>
|
|
136
|
+
<div
|
|
137
|
+
className={combinedCls(
|
|
138
|
+
'splitter-panel',
|
|
139
|
+
clsWrite(wrapperClassName, '')
|
|
140
|
+
)}
|
|
141
|
+
ref={containerRef}
|
|
142
|
+
>
|
|
143
|
+
|
|
144
|
+
<div
|
|
145
|
+
className="splitter-panel-left"
|
|
146
|
+
style={{ "--splitter-panel-left-w": `${leftWidth}px` } as React.CSSProperties}
|
|
147
|
+
>
|
|
148
|
+
{left}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/*<!--DIVIDER-->*/}
|
|
152
|
+
<div
|
|
153
|
+
ref={dividerRef}
|
|
154
|
+
className={`splitter-panel-divider ${dragging ? 'dragging' : ''}`}
|
|
155
|
+
onMouseDown={handleMouseDownSplitPanels}
|
|
156
|
+
onTouchStart={handleTouchStartSplitPanels}
|
|
157
|
+
onTouchEnd={handleMouseUpSplitPanels}
|
|
158
|
+
/>
|
|
159
|
+
{/*<!--/DIVIDER-->*/}
|
|
160
|
+
|
|
161
|
+
<div className="splitter-panel-right">
|
|
162
|
+
{right}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</>}
|
|
166
|
+
|
|
167
|
+
</>
|
|
168
|
+
)
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
SplitterPanel.Left = SplitterPanelLeft;
|
|
172
|
+
SplitterPanel.Right = SplitterPanelRight;
|
|
173
|
+
|
|
174
|
+
export default SplitterPanel;
|
|
@@ -24,9 +24,10 @@ const App = () => {
|
|
|
24
24
|
import { useEffect, useState } from 'react';
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
const useIsMobile = (breakpoint
|
|
28
|
-
const [isMobile, setIsMobile] = useState
|
|
29
|
-
const [isMounted, setIsMounted] = useState
|
|
27
|
+
const useIsMobile = (breakpoint = 600) => {
|
|
28
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
29
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
30
|
+
|
|
30
31
|
|
|
31
32
|
useEffect(() => {
|
|
32
33
|
// Set the mounted state to true after the component has mounted
|
|
@@ -34,7 +35,92 @@ const useIsMobile = (breakpoint: number = 768): boolean => {
|
|
|
34
35
|
|
|
35
36
|
const handleResize = () => {
|
|
36
37
|
if (window) {
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const detectDeviceType = () => {
|
|
41
|
+
// 1. First check if window and navigator are available (SSR compatibility)
|
|
42
|
+
if (typeof window === 'undefined' || !navigator) {
|
|
43
|
+
return 'desktop'; // Default to desktop
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Get user agent string
|
|
47
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
48
|
+
|
|
49
|
+
// 3. Get platform info
|
|
50
|
+
const platform = navigator.platform.toLowerCase();
|
|
51
|
+
|
|
52
|
+
// 4. Check screen characteristics using window.matchMedia
|
|
53
|
+
const isTouch = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
|
|
54
|
+
|
|
55
|
+
const isPortrait = window.matchMedia('(orientation: portrait)').matches;
|
|
56
|
+
const isLandscape = window.matchMedia('(orientation: landscape)').matches;
|
|
57
|
+
|
|
58
|
+
// 5. Get screen dimensions
|
|
59
|
+
const screenWidth = window.screen.width;
|
|
60
|
+
const screenHeight = window.screen.height;
|
|
61
|
+
const minScreenSize = Math.min(screenWidth, screenHeight);
|
|
62
|
+
const maxScreenSize = Math.max(screenWidth, screenHeight);
|
|
63
|
+
|
|
64
|
+
// Define device characteristics
|
|
65
|
+
const isTablet = (
|
|
66
|
+
// Traditional UA detection
|
|
67
|
+
/ipad/.test(ua) ||
|
|
68
|
+
(/android/.test(ua) && !/mobile/.test(ua)) ||
|
|
69
|
+
/tablet/.test(ua) ||
|
|
70
|
+
/playbook/.test(ua) ||
|
|
71
|
+
/nexus (7|9|10)/.test(ua) ||
|
|
72
|
+
/sm-t/.test(ua) ||
|
|
73
|
+
/huawei(.*)mediapad/.test(ua) ||
|
|
74
|
+
|
|
75
|
+
// Special detection for iPad Pro and newer iPads
|
|
76
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
|
|
77
|
+
|
|
78
|
+
// Screen size characteristics (tablets typically fall within this range)
|
|
79
|
+
(minScreenSize >= breakpoint && maxScreenSize <= 1366 && isTouch) ||
|
|
80
|
+
|
|
81
|
+
// Specific device detection
|
|
82
|
+
/kindle|silk|kftt|kfot|kfjwa|kfjwi|kfsowi|kfthwa|kfthwi|kfapwa|kfapwi/i.test(ua)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const isMobile = (
|
|
86
|
+
!isTablet && ( // Prevent tablets from being detected as phones
|
|
87
|
+
// Traditional mobile device detection
|
|
88
|
+
/iphone|ipod|android.*mobile|windows phone|mobi/.test(ua) ||
|
|
89
|
+
|
|
90
|
+
// Screen size characteristics (phones typically smaller than 600px)
|
|
91
|
+
(minScreenSize < breakpoint && isTouch) ||
|
|
92
|
+
|
|
93
|
+
// Additional mobile device detection
|
|
94
|
+
/blackberry|\bbb\d+|meego|webos|palm|phone|pocket|mobile|mini|iemobile/i.test(ua)
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// 6. Comprehensive decision logic
|
|
99
|
+
if (isMobile) {
|
|
100
|
+
// Additional check for small tablets
|
|
101
|
+
if (maxScreenSize >= 1024 && isTouch) {
|
|
102
|
+
return 'tablet';
|
|
103
|
+
}
|
|
104
|
+
return 'mobile';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isTablet) {
|
|
108
|
+
// Additional check for touch-enabled laptops
|
|
109
|
+
if (maxScreenSize > 1366 && /windows/.test(ua)) {
|
|
110
|
+
return 'desktop';
|
|
111
|
+
}
|
|
112
|
+
return 'tablet';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 7. Check for touch-enabled laptops
|
|
116
|
+
if (isTouch && /windows/.test(ua) && maxScreenSize > 1366) {
|
|
117
|
+
return 'desktop';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 'desktop';
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
setIsMobile(detectDeviceType() === 'mobile');
|
|
38
124
|
}
|
|
39
125
|
};
|
|
40
126
|
|
|
@@ -205,6 +205,25 @@ function getSpecifiedDate(v: Date | string, days: number): string {
|
|
|
205
205
|
return specifiedDay;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Calculates the total number of days from today going back a specified number of months.
|
|
210
|
+
*
|
|
211
|
+
* @param {number} monthsAgo - The number of months to go back (e.g., 3 means the past 3 months).
|
|
212
|
+
* @returns {number} The total number of days between the calculated past date and today.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* getDaysInLastMonths(3); // Returns number of days in the past 3 months
|
|
216
|
+
*/
|
|
217
|
+
function getDaysInLastMonths(monthsAgo: number = 3): number {
|
|
218
|
+
const today: Date = new Date();
|
|
219
|
+
const pastDate: Date = new Date();
|
|
220
|
+
pastDate.setMonth(today.getMonth() - monthsAgo);
|
|
221
|
+
|
|
222
|
+
const diffInMs: number = today.getTime() - pastDate.getTime();
|
|
223
|
+
const diffInDays: number = Math.round(diffInMs / (1000 * 60 * 60 * 24));
|
|
224
|
+
|
|
225
|
+
return diffInDays;
|
|
226
|
+
}
|
|
208
227
|
|
|
209
228
|
|
|
210
229
|
/**
|
|
@@ -304,28 +323,26 @@ function getCurrentYear(): number {
|
|
|
304
323
|
/**
|
|
305
324
|
* Get current month
|
|
306
325
|
* @param {Boolean} padZeroEnabled
|
|
307
|
-
* @returns {Number}
|
|
326
|
+
* @returns {Number|String}
|
|
308
327
|
*/
|
|
309
|
-
function getCurrentMonth(padZeroEnabled: boolean = true): number {
|
|
310
|
-
const m:
|
|
328
|
+
function getCurrentMonth(padZeroEnabled: boolean = true): string | number {
|
|
329
|
+
const m: number = new Date().getMonth() + 1;
|
|
311
330
|
return padZeroEnabled ? String(m).padStart(2, '0') : m;
|
|
312
331
|
}
|
|
313
332
|
|
|
314
333
|
|
|
315
|
-
|
|
316
334
|
/**
|
|
317
335
|
* Get current day
|
|
318
336
|
* @param {Boolean} padZeroEnabled
|
|
319
|
-
* @returns {Number}
|
|
337
|
+
* @returns {Number|String}
|
|
320
338
|
*/
|
|
321
|
-
function getCurrentDay(padZeroEnabled: boolean = true): number {
|
|
322
|
-
const d:
|
|
339
|
+
function getCurrentDay(padZeroEnabled: boolean = true): string | number {
|
|
340
|
+
const d: number = new Date().getDate();
|
|
323
341
|
return padZeroEnabled ? String(d).padStart(2, '0') : d;
|
|
324
342
|
}
|
|
325
343
|
|
|
326
344
|
|
|
327
345
|
|
|
328
|
-
|
|
329
346
|
/**
|
|
330
347
|
* Get first and last month day
|
|
331
348
|
* @param {Number} v
|
|
@@ -562,7 +579,10 @@ export {
|
|
|
562
579
|
getPrevMonthDate,
|
|
563
580
|
getNextYearDate,
|
|
564
581
|
getPrevYearDate,
|
|
582
|
+
|
|
583
|
+
//
|
|
565
584
|
getSpecifiedDate,
|
|
585
|
+
getDaysInLastMonths,
|
|
566
586
|
|
|
567
587
|
|
|
568
588
|
// convert
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get timeslots from starting and ending time
|
|
3
|
+
* @param {string} startTime - start time in format "HH:mm"
|
|
4
|
+
* @param {string} endTime - end time in format "HH:mm"
|
|
5
|
+
* @param {number} timeInterval - time interval in minutes
|
|
6
|
+
* @param {boolean} formatRange - if true returns ranges like "10:00 - 11:00", if false returns single times like "10:00"
|
|
7
|
+
* @returns {string[]} Array of time slots
|
|
8
|
+
* @example
|
|
9
|
+
|
|
10
|
+
console.log(getTimeslots("10:00", "14:00", 60, true)); //['10:00 - 11:00', '11:00 - 12:00', '12:00 - 13:00', '13:00 - 14:00']
|
|
11
|
+
console.log(getTimeslots("10:00", "14:00", 60)); // ['10:00', '11:00', '12:00', '13:00']
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function getTimeslots(
|
|
15
|
+
startTime: string,
|
|
16
|
+
endTime: string,
|
|
17
|
+
timeInterval: number,
|
|
18
|
+
formatRange: boolean = false
|
|
19
|
+
): string[] {
|
|
20
|
+
const parseTime = (s: string): number => {
|
|
21
|
+
const c = s.split(':');
|
|
22
|
+
return parseInt(c[0]) * 60 + parseInt(c[1]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const convertHours = (mins: number): string => {
|
|
26
|
+
const hour = Math.floor(mins / 60);
|
|
27
|
+
mins = Math.trunc(mins % 60);
|
|
28
|
+
const converted = pad(hour, 2) + ':' + pad(mins, 2);
|
|
29
|
+
return converted;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pad = (str: string | number, max: number): string => {
|
|
33
|
+
str = str.toString();
|
|
34
|
+
return str.length < max ? pad("0" + str, max) : str;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// calculate time slot
|
|
38
|
+
const calculateTimeSlot = (_startTime: number, _endTime: number, _timeInterval: number): string[] => {
|
|
39
|
+
const timeSlots: string[] = [];
|
|
40
|
+
// Round start and end times to next 30 min interval
|
|
41
|
+
_startTime = Math.ceil(_startTime / 30) * 30;
|
|
42
|
+
_endTime = Math.ceil(_endTime / 30) * 30;
|
|
43
|
+
|
|
44
|
+
// Start and end of interval in the loop
|
|
45
|
+
let currentTime = _startTime;
|
|
46
|
+
while (currentTime < _endTime) {
|
|
47
|
+
if (formatRange) {
|
|
48
|
+
const t = convertHours(currentTime) + ' - ' + convertHours(currentTime + _timeInterval);
|
|
49
|
+
timeSlots.push(t);
|
|
50
|
+
} else {
|
|
51
|
+
timeSlots.push(convertHours(currentTime));
|
|
52
|
+
}
|
|
53
|
+
currentTime += _timeInterval;
|
|
54
|
+
}
|
|
55
|
+
return timeSlots;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const inputEndTime = parseTime(endTime);
|
|
59
|
+
const inputStartTime = parseTime(startTime);
|
|
60
|
+
const timeSegment = calculateTimeSlot(inputStartTime, inputEndTime, timeInterval);
|
|
61
|
+
|
|
62
|
+
return timeSegment;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get minutes between two dates
|
|
68
|
+
* @param {Date} startDate - start date
|
|
69
|
+
* @param {Date} endDate - ebd date
|
|
70
|
+
* @returns Number
|
|
71
|
+
*/
|
|
72
|
+
function getMinutesBetweenDates(startDate, endDate) {
|
|
73
|
+
const diff = endDate.getTime() - startDate.getTime();
|
|
74
|
+
return (diff / 60000);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get minutes between two time
|
|
80
|
+
* @param {String} startTime - start time
|
|
81
|
+
* @param {String} endTime - ebd time
|
|
82
|
+
* @returns Number
|
|
83
|
+
*/
|
|
84
|
+
function getMinutesBetweenTime(startTime, endTime) {
|
|
85
|
+
const pad = (num) => {
|
|
86
|
+
return ("0" + num).slice(-2);
|
|
87
|
+
};
|
|
88
|
+
let s = startTime.split(":"), sMin = +s[1] + s[0] * 60,
|
|
89
|
+
e = endTime.split(":"), eMin = +e[1] + e[0] * 60,
|
|
90
|
+
diff = eMin - sMin;
|
|
91
|
+
|
|
92
|
+
if (diff < 0) { sMin -= 12 * 60; diff = eMin - sMin }
|
|
93
|
+
const h = Math.floor(diff / 60),
|
|
94
|
+
m = diff % 60;
|
|
95
|
+
return "" + pad(h) + ":" + pad(m);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert HH:MM:SS into minute
|
|
102
|
+
* @param {String} timeStr - time string
|
|
103
|
+
* @returns Number
|
|
104
|
+
*/
|
|
105
|
+
function convertTimeToMin(timeStr) {
|
|
106
|
+
const _time = timeStr.split(':').length === 3 ? `${timeStr}` : `${timeStr}:00`;
|
|
107
|
+
|
|
108
|
+
const res = _time.split(':'); // split it at the colons
|
|
109
|
+
|
|
110
|
+
// Hours are worth 60 minutes.
|
|
111
|
+
const minutes = (+res[0]) * 60 + (+res[1]);
|
|
112
|
+
return minutes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
getTimeslots,
|
|
118
|
+
getMinutesBetweenDates,
|
|
119
|
+
getMinutesBetweenTime,
|
|
120
|
+
convertTimeToMin
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
package/lib/esm/index.js
CHANGED
|
@@ -27,12 +27,14 @@ export { default as NumberInput } from './NumberInput';
|
|
|
27
27
|
export { default as Pagination } from './Pagination';
|
|
28
28
|
export { default as Radio } from './Radio';
|
|
29
29
|
export { default as RangeSlider } from './RangeSlider';
|
|
30
|
+
export { default as Refresher } from './Refresher';
|
|
30
31
|
export { default as RootPortal } from './RootPortal';
|
|
31
32
|
export { default as ScrollReveal } from './ScrollReveal';
|
|
32
33
|
export { default as Scrollbar } from './Scrollbar';
|
|
33
34
|
export { default as SearchBar } from './SearchBar';
|
|
34
35
|
export { default as Select } from './Select';
|
|
35
36
|
export { default as ShowMoreLess } from './ShowMoreLess';
|
|
37
|
+
export { default as SplitterPanel } from './SplitterPanel';
|
|
36
38
|
export { default as Stepper } from './Stepper';
|
|
37
39
|
export { default as Switch } from './Switch';
|
|
38
40
|
export { default as Table } from './Table';
|