@koslibs/components 1.15.0 → 1.16.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/CHANGELOG.md +8 -0
- package/dist/components/Datepicker/components/calendar-month.d.ts +8 -0
- package/dist/components/Datepicker/components/calendar-month.js +68 -0
- package/dist/components/Datepicker/components/range-calendar.d.ts +5 -0
- package/dist/components/Datepicker/components/range-calendar.js +78 -0
- package/dist/components/Datepicker/datepicker-panel.d.ts +2 -0
- package/dist/components/Datepicker/datepicker-panel.js +187 -0
- package/dist/components/Datepicker/datepicker.d.ts +2 -0
- package/dist/components/Datepicker/datepicker.js +194 -0
- package/dist/components/Datepicker/hooks/use-datepicker-draft.d.ts +22 -0
- package/dist/components/Datepicker/hooks/use-datepicker-draft.js +105 -0
- package/dist/components/Datepicker/hooks/use-datepicker-state.d.ts +23 -0
- package/dist/components/Datepicker/hooks/use-datepicker-state.js +106 -0
- package/dist/components/Datepicker/hooks/use-datepicker-value.d.ts +8 -0
- package/dist/components/Datepicker/hooks/use-datepicker-value.js +31 -0
- package/dist/components/Datepicker/hooks/use-range-scroll.d.ts +15 -0
- package/dist/components/Datepicker/hooks/use-range-scroll.js +219 -0
- package/dist/components/Datepicker/index.d.ts +3 -0
- package/dist/components/Datepicker/index.js +2 -0
- package/dist/components/Datepicker/index.module.js +37 -0
- package/dist/components/Datepicker/index_module.css +251 -0
- package/dist/components/Datepicker/panel.module.js +39 -0
- package/dist/components/Datepicker/panel_module.css +375 -0
- package/dist/components/Datepicker/types.d.ts +133 -0
- package/dist/components/Datepicker/types.js +0 -0
- package/dist/components/Datepicker/utils/date-utils.d.ts +17 -0
- package/dist/components/Datepicker/utils/date-utils.js +80 -0
- package/dist/components/Datepicker/utils/range-scroll-geometry.d.ts +15 -0
- package/dist/components/Datepicker/utils/range-scroll-geometry.js +29 -0
- package/dist/components/Datepicker/vars.css +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { clampMonth, countDays, getMonthIndex, normalizeValue, parseDate, today } from "../utils/date-utils.js";
|
|
3
|
+
function useDatepickerDraft(mode, committed) {
|
|
4
|
+
const from = 'string' == typeof committed ? committed : committed?.from ?? '';
|
|
5
|
+
const to = 'object' == typeof committed ? committed?.to ?? '' : '';
|
|
6
|
+
const [draft, setDraft] = useState({
|
|
7
|
+
from,
|
|
8
|
+
to
|
|
9
|
+
});
|
|
10
|
+
const [preview, setPreview] = useState(null);
|
|
11
|
+
const [month, setMonth] = useState(()=>clampMonth(getMonthIndex(from || today()), 'range' === mode));
|
|
12
|
+
const [touched, setTouched] = useState({
|
|
13
|
+
from: false,
|
|
14
|
+
to: false
|
|
15
|
+
});
|
|
16
|
+
const [navigationVersion, setNavigationVersion] = useState(0);
|
|
17
|
+
useEffect(()=>{
|
|
18
|
+
setDraft({
|
|
19
|
+
from,
|
|
20
|
+
to
|
|
21
|
+
});
|
|
22
|
+
setTouched({
|
|
23
|
+
from: false,
|
|
24
|
+
to: false
|
|
25
|
+
});
|
|
26
|
+
setPreview(null);
|
|
27
|
+
setMonth(clampMonth(getMonthIndex(from || today()), 'range' === mode));
|
|
28
|
+
setNavigationVersion((current)=>current + 1);
|
|
29
|
+
}, [
|
|
30
|
+
from,
|
|
31
|
+
to,
|
|
32
|
+
mode
|
|
33
|
+
]);
|
|
34
|
+
const value = normalizeValue(mode, 'single' === mode ? draft.from : draft);
|
|
35
|
+
let toError;
|
|
36
|
+
if (!touched.to || draft.to && parseDate(draft.to)) {
|
|
37
|
+
if (parseDate(draft.from) && parseDate(draft.to) && draft.to < draft.from) toError = 'End date must not precede start date';
|
|
38
|
+
} else toError = 'Enter a valid date (YYYY-MM-DD)';
|
|
39
|
+
const errors = {
|
|
40
|
+
from: !touched.from || draft.from && parseDate(draft.from) ? void 0 : 'Enter a valid date (YYYY-MM-DD)',
|
|
41
|
+
to: toError
|
|
42
|
+
};
|
|
43
|
+
function navigate(index) {
|
|
44
|
+
setMonth(clampMonth(index, 'range' === mode));
|
|
45
|
+
setNavigationVersion((current)=>current + 1);
|
|
46
|
+
}
|
|
47
|
+
function reset() {
|
|
48
|
+
setDraft({
|
|
49
|
+
from,
|
|
50
|
+
to
|
|
51
|
+
});
|
|
52
|
+
setPreview(null);
|
|
53
|
+
setTouched({
|
|
54
|
+
from: false,
|
|
55
|
+
to: false
|
|
56
|
+
});
|
|
57
|
+
navigate(getMonthIndex(from || today()));
|
|
58
|
+
}
|
|
59
|
+
function input(field, text) {
|
|
60
|
+
setDraft((current)=>({
|
|
61
|
+
...current,
|
|
62
|
+
[field]: text
|
|
63
|
+
}));
|
|
64
|
+
setTouched((current)=>({
|
|
65
|
+
...current,
|
|
66
|
+
[field]: true
|
|
67
|
+
}));
|
|
68
|
+
setPreview(null);
|
|
69
|
+
if (parseDate(text)) navigate(getMonthIndex(text));
|
|
70
|
+
}
|
|
71
|
+
function select(date) {
|
|
72
|
+
'single' === mode || !parseDate(draft.from) || value || date < draft.from ? setDraft({
|
|
73
|
+
from: date,
|
|
74
|
+
to: ''
|
|
75
|
+
}) : setDraft({
|
|
76
|
+
from: draft.from,
|
|
77
|
+
to: date
|
|
78
|
+
});
|
|
79
|
+
setTouched({
|
|
80
|
+
from: false,
|
|
81
|
+
to: false
|
|
82
|
+
});
|
|
83
|
+
setPreview(null);
|
|
84
|
+
}
|
|
85
|
+
let count = 0;
|
|
86
|
+
if ('string' == typeof value) count = 1;
|
|
87
|
+
else if (value) count = countDays(value.from, value.to);
|
|
88
|
+
return {
|
|
89
|
+
draft,
|
|
90
|
+
value,
|
|
91
|
+
errors,
|
|
92
|
+
month,
|
|
93
|
+
navigationVersion,
|
|
94
|
+
preview,
|
|
95
|
+
navigate,
|
|
96
|
+
reset,
|
|
97
|
+
input,
|
|
98
|
+
select,
|
|
99
|
+
count,
|
|
100
|
+
hover (date) {
|
|
101
|
+
setPreview('range' === mode && parseDate(draft.from) && !value && date && date >= draft.from ? date : null);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export { useDatepickerDraft };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type FocusEvent, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent } from 'react';
|
|
2
|
+
import type { DatepickerProps, DatepickerRange } from '../types.js';
|
|
3
|
+
export declare function useDatepickerState(props: DatepickerProps): {
|
|
4
|
+
mode: "range" | "single";
|
|
5
|
+
value: string | DatepickerRange | null;
|
|
6
|
+
open: boolean;
|
|
7
|
+
blocked: boolean;
|
|
8
|
+
buttonRef: import("react").RefObject<HTMLButtonElement>;
|
|
9
|
+
popupRef: import("react").RefObject<HTMLDivElement>;
|
|
10
|
+
anchor: HTMLDivElement | null;
|
|
11
|
+
setAnchor: import("react").Dispatch<import("react").SetStateAction<HTMLDivElement | null>>;
|
|
12
|
+
session: number;
|
|
13
|
+
close: (restoreFocus?: boolean) => void;
|
|
14
|
+
apply: (event: MouseEvent<HTMLButtonElement>, next: string | DatepickerRange | null) => void;
|
|
15
|
+
raisedLabel: boolean;
|
|
16
|
+
handleKeyDown(event: ReactKeyboardEvent<HTMLElement>): void;
|
|
17
|
+
triggerProps: {
|
|
18
|
+
onClick(event: MouseEvent<HTMLButtonElement>): void;
|
|
19
|
+
onFocus(event: FocusEvent<HTMLButtonElement>): void;
|
|
20
|
+
onBlur(event: FocusEvent<HTMLButtonElement>): void;
|
|
21
|
+
};
|
|
22
|
+
clear(event: MouseEvent<HTMLButtonElement>): void;
|
|
23
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useDatepickerValue } from "./use-datepicker-value.js";
|
|
3
|
+
function useDatepickerState(props) {
|
|
4
|
+
const mode = props.mode ?? 'single';
|
|
5
|
+
const selection = useDatepickerValue(props);
|
|
6
|
+
const [localOpen, setLocalOpen] = useState(props.defaultOpen ?? false);
|
|
7
|
+
const [focused, setFocused] = useState(false);
|
|
8
|
+
const [session, setSession] = useState(0);
|
|
9
|
+
const [anchor, setAnchor] = useState(null);
|
|
10
|
+
const buttonRef = useRef(null);
|
|
11
|
+
const popupRef = useRef(null);
|
|
12
|
+
const value = selection.value;
|
|
13
|
+
const blocked = Boolean(props.disabled || props.readOnly);
|
|
14
|
+
const open = !blocked && (props.open ?? localOpen);
|
|
15
|
+
function setOpen(next) {
|
|
16
|
+
if (next && blocked || next === open) return;
|
|
17
|
+
if (void 0 === props.open) setLocalOpen(next);
|
|
18
|
+
props.onOpenChange?.(next);
|
|
19
|
+
}
|
|
20
|
+
function close(restoreFocus = false) {
|
|
21
|
+
setOpen(false);
|
|
22
|
+
setSession((current)=>current + 1);
|
|
23
|
+
if (restoreFocus) buttonRef.current?.focus();
|
|
24
|
+
}
|
|
25
|
+
function apply(event, next) {
|
|
26
|
+
if (blocked || !selection.commit(event, next)) return;
|
|
27
|
+
close(true);
|
|
28
|
+
}
|
|
29
|
+
useEffect(()=>{
|
|
30
|
+
if (!open) return;
|
|
31
|
+
const ownerDocument = buttonRef.current?.ownerDocument;
|
|
32
|
+
const escape = (event)=>{
|
|
33
|
+
if ('Escape' !== event.key || event.defaultPrevented) return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
close(true);
|
|
37
|
+
};
|
|
38
|
+
ownerDocument?.addEventListener('keydown', escape);
|
|
39
|
+
return ()=>ownerDocument?.removeEventListener('keydown', escape);
|
|
40
|
+
});
|
|
41
|
+
const resetRef = useRef();
|
|
42
|
+
resetRef.current = ()=>{
|
|
43
|
+
selection.reset();
|
|
44
|
+
close();
|
|
45
|
+
};
|
|
46
|
+
useEffect(()=>{
|
|
47
|
+
const form = buttonRef.current?.form;
|
|
48
|
+
let active = true;
|
|
49
|
+
const reset = (event)=>{
|
|
50
|
+
queueMicrotask(()=>{
|
|
51
|
+
if (active && !event.defaultPrevented) resetRef.current?.();
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
form?.addEventListener('reset', reset);
|
|
55
|
+
return ()=>{
|
|
56
|
+
active = false;
|
|
57
|
+
form?.removeEventListener('reset', reset);
|
|
58
|
+
};
|
|
59
|
+
}, [
|
|
60
|
+
props.form
|
|
61
|
+
]);
|
|
62
|
+
return {
|
|
63
|
+
mode,
|
|
64
|
+
value,
|
|
65
|
+
open,
|
|
66
|
+
blocked,
|
|
67
|
+
buttonRef,
|
|
68
|
+
popupRef,
|
|
69
|
+
anchor,
|
|
70
|
+
setAnchor,
|
|
71
|
+
session,
|
|
72
|
+
close,
|
|
73
|
+
apply,
|
|
74
|
+
raisedLabel: Boolean(value || open || focused && !blocked),
|
|
75
|
+
handleKeyDown (event) {
|
|
76
|
+
if ('Escape' !== event.key || event.defaultPrevented || !open) return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
close(true);
|
|
80
|
+
},
|
|
81
|
+
triggerProps: {
|
|
82
|
+
onClick (event) {
|
|
83
|
+
props.onClick?.(event);
|
|
84
|
+
if (event.defaultPrevented || blocked) return;
|
|
85
|
+
if (open) close();
|
|
86
|
+
else setOpen(true);
|
|
87
|
+
},
|
|
88
|
+
onFocus (event) {
|
|
89
|
+
setFocused(true);
|
|
90
|
+
props.onFocus?.(event);
|
|
91
|
+
},
|
|
92
|
+
onBlur (event) {
|
|
93
|
+
setFocused(false);
|
|
94
|
+
props.onBlur?.(event);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
clear (event) {
|
|
98
|
+
if (blocked) return;
|
|
99
|
+
selection.clear();
|
|
100
|
+
close();
|
|
101
|
+
props.onClear?.(event);
|
|
102
|
+
buttonRef.current?.focus();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export { useDatepickerState };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type MouseEvent } from 'react';
|
|
2
|
+
import type { DatepickerRange, DatepickerValueProps } from '../types.js';
|
|
3
|
+
export declare function useDatepickerValue(props: DatepickerValueProps): {
|
|
4
|
+
value: string | DatepickerRange | null;
|
|
5
|
+
commit(event: MouseEvent<HTMLButtonElement>, next: string | DatepickerRange | null): boolean;
|
|
6
|
+
clear(): void;
|
|
7
|
+
reset(): void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { normalizeValue } from "../utils/date-utils.js";
|
|
3
|
+
function useDatepickerValue(props) {
|
|
4
|
+
const mode = props.mode ?? 'single';
|
|
5
|
+
const controlled = void 0 !== props.value;
|
|
6
|
+
const [localValue, setLocalValue] = useState(()=>normalizeValue(mode, props.defaultValue));
|
|
7
|
+
const value = normalizeValue(mode, controlled ? props.value : localValue);
|
|
8
|
+
return {
|
|
9
|
+
value,
|
|
10
|
+
commit (event, next) {
|
|
11
|
+
const normalized = normalizeValue(mode, next);
|
|
12
|
+
if (!normalized) return false;
|
|
13
|
+
if (!controlled) setLocalValue(normalized);
|
|
14
|
+
if ('range' === props.mode) {
|
|
15
|
+
if ('object' == typeof normalized) props.onChange?.(event, {
|
|
16
|
+
value: normalized
|
|
17
|
+
});
|
|
18
|
+
} else if ('string' == typeof normalized) props.onChange?.(event, {
|
|
19
|
+
value: normalized
|
|
20
|
+
});
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
clear () {
|
|
24
|
+
if (!controlled) setLocalValue(null);
|
|
25
|
+
},
|
|
26
|
+
reset () {
|
|
27
|
+
if (!controlled) setLocalValue(normalizeValue(mode, props.defaultValue));
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export { useDatepickerValue };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function useRangeScroll(month: number, navigationVersion: number): {
|
|
2
|
+
calendarRef: import("react").RefObject<HTMLDivElement>;
|
|
3
|
+
monthsRef: import("react").RefObject<HTMLDivElement>;
|
|
4
|
+
windowRef: import("react").RefObject<HTMLDivElement>;
|
|
5
|
+
registerCalendarMonth(monthIndex: number, element: HTMLDivElement | null): void;
|
|
6
|
+
registerMonth(monthIndex: number, element: HTMLButtonElement | null): void;
|
|
7
|
+
visible: {
|
|
8
|
+
first: number;
|
|
9
|
+
last: number;
|
|
10
|
+
};
|
|
11
|
+
calendarScroll: () => void;
|
|
12
|
+
monthsScroll: () => void;
|
|
13
|
+
calendars: number[];
|
|
14
|
+
months: number[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { LAST_MONTH } from "../utils/date-utils.js";
|
|
3
|
+
import { measureMonths, offsetFor, positionAt } from "../utils/range-scroll-geometry.js";
|
|
4
|
+
const CALENDAR_BUFFER = 12;
|
|
5
|
+
const MONTH_BUFFER = 36;
|
|
6
|
+
const calendarStartFor = (month)=>Math.max(0, Math.min(LAST_MONTH - CALENDAR_BUFFER + 1, month - 6));
|
|
7
|
+
const monthStartFor = (month)=>Math.max(0, Math.min(LAST_MONTH - MONTH_BUFFER + 1, month - 12));
|
|
8
|
+
function setScroll(viewport, offset) {
|
|
9
|
+
viewport.scrollTop = Math.max(0, Math.min(offset, viewport.scrollHeight - viewport.clientHeight));
|
|
10
|
+
}
|
|
11
|
+
function useRangeScroll(month, navigationVersion) {
|
|
12
|
+
const [calendarStart, setCalendarStart] = useState(()=>calendarStartFor(month));
|
|
13
|
+
const [monthStart, setMonthStart] = useState(()=>monthStartFor(month));
|
|
14
|
+
const [visible, setVisible] = useState({
|
|
15
|
+
first: month,
|
|
16
|
+
last: month + 1
|
|
17
|
+
});
|
|
18
|
+
const calendarRef = useRef(null);
|
|
19
|
+
const monthsRef = useRef(null);
|
|
20
|
+
const windowRef = useRef(null);
|
|
21
|
+
const calendarElements = useRef(new Map());
|
|
22
|
+
const monthElements = useRef(new Map());
|
|
23
|
+
const navigation = useRef('');
|
|
24
|
+
const pendingPosition = useRef(null);
|
|
25
|
+
const monthAnchor = useRef(null);
|
|
26
|
+
const calendarTop = useRef(0);
|
|
27
|
+
const monthsTop = useRef(0);
|
|
28
|
+
const windowTop = useRef(0);
|
|
29
|
+
function readGeometry() {
|
|
30
|
+
const viewport = calendarRef.current;
|
|
31
|
+
const list = monthsRef.current;
|
|
32
|
+
return viewport && list ? {
|
|
33
|
+
calendar: measureMonths(calendarElements.current),
|
|
34
|
+
months: measureMonths(monthElements.current)
|
|
35
|
+
} : null;
|
|
36
|
+
}
|
|
37
|
+
function cancelPendingSeek() {
|
|
38
|
+
if (!pendingPosition.current) return;
|
|
39
|
+
pendingPosition.current = null;
|
|
40
|
+
setCalendarStart(calendarStart);
|
|
41
|
+
}
|
|
42
|
+
function recenterMonthList(target, months) {
|
|
43
|
+
const list = monthsRef.current;
|
|
44
|
+
const targetStart = monthStartFor(target);
|
|
45
|
+
if (!list || targetStart === monthStart) return false;
|
|
46
|
+
const anchor = months.find((row)=>row.month === target);
|
|
47
|
+
monthAnchor.current = anchor ? {
|
|
48
|
+
month: target,
|
|
49
|
+
offset: anchor.start - list.scrollTop
|
|
50
|
+
} : null;
|
|
51
|
+
setMonthStart(targetStart);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function syncFromCalendar(geometry = readGeometry()) {
|
|
55
|
+
const viewport = calendarRef.current;
|
|
56
|
+
const list = monthsRef.current;
|
|
57
|
+
const indicator = windowRef.current;
|
|
58
|
+
if (!viewport || !list || !indicator || !geometry) return;
|
|
59
|
+
calendarTop.current = viewport.scrollTop;
|
|
60
|
+
const children = geometry.calendar;
|
|
61
|
+
const first = children.find((child)=>child.contentEnd > viewport.scrollTop + 1) ?? children[children.length - 1];
|
|
62
|
+
const last = children.findLast((child)=>child.start < viewport.scrollTop + viewport.clientHeight - 1) ?? first;
|
|
63
|
+
if (first && last && viewport.clientHeight) {
|
|
64
|
+
const next = {
|
|
65
|
+
first: first.month,
|
|
66
|
+
last: last.month
|
|
67
|
+
};
|
|
68
|
+
setVisible((current)=>current.first === next.first && current.last === next.last ? current : next);
|
|
69
|
+
}
|
|
70
|
+
const start = positionAt(children, viewport.scrollTop);
|
|
71
|
+
const end = positionAt(children, viewport.scrollTop + viewport.clientHeight);
|
|
72
|
+
if (!start || !end) return;
|
|
73
|
+
const top = offsetFor(geometry.months, start);
|
|
74
|
+
const bottom = offsetFor(geometry.months, end);
|
|
75
|
+
if (null === top || null === bottom) return void recenterMonthList(start.month, geometry.months);
|
|
76
|
+
indicator.style.top = top + 'px';
|
|
77
|
+
indicator.style.height = Math.max(0, bottom - top) + 'px';
|
|
78
|
+
windowTop.current = top;
|
|
79
|
+
const centeredTop = (top + bottom - list.clientHeight) / 2;
|
|
80
|
+
if ((centeredTop < 0 || centeredTop > list.scrollHeight - list.clientHeight) && recenterMonthList(start.month, geometry.months)) return;
|
|
81
|
+
setScroll(list, centeredTop);
|
|
82
|
+
monthsTop.current = list.scrollTop;
|
|
83
|
+
}
|
|
84
|
+
useLayoutEffect(()=>{
|
|
85
|
+
const viewport = calendarRef.current;
|
|
86
|
+
const list = monthsRef.current;
|
|
87
|
+
if (!viewport || !list) return;
|
|
88
|
+
const key = month + ':' + navigationVersion;
|
|
89
|
+
if (navigation.current !== key) {
|
|
90
|
+
navigation.current = key;
|
|
91
|
+
pendingPosition.current = {
|
|
92
|
+
month,
|
|
93
|
+
fraction: 0
|
|
94
|
+
};
|
|
95
|
+
const targetStart = calendarStartFor(month);
|
|
96
|
+
if (targetStart !== calendarStart) return void setCalendarStart(targetStart);
|
|
97
|
+
}
|
|
98
|
+
const geometry = readGeometry();
|
|
99
|
+
if (!geometry) return;
|
|
100
|
+
if (monthAnchor.current) {
|
|
101
|
+
const { month: anchorMonth, offset } = monthAnchor.current;
|
|
102
|
+
const anchor = geometry.months.find((row)=>row.month === anchorMonth);
|
|
103
|
+
if (anchor) setScroll(list, anchor.start - offset);
|
|
104
|
+
monthAnchor.current = null;
|
|
105
|
+
monthsTop.current = list.scrollTop;
|
|
106
|
+
}
|
|
107
|
+
if (pendingPosition.current) {
|
|
108
|
+
const target = offsetFor(geometry.calendar, pendingPosition.current);
|
|
109
|
+
if (null === target) return void setCalendarStart(calendarStartFor(pendingPosition.current.month));
|
|
110
|
+
setScroll(viewport, target);
|
|
111
|
+
pendingPosition.current = null;
|
|
112
|
+
}
|
|
113
|
+
syncFromCalendar(geometry);
|
|
114
|
+
}, [
|
|
115
|
+
month,
|
|
116
|
+
navigationVersion,
|
|
117
|
+
calendarStart,
|
|
118
|
+
monthStart
|
|
119
|
+
]);
|
|
120
|
+
useLayoutEffect(()=>{
|
|
121
|
+
const viewport = calendarRef.current;
|
|
122
|
+
const list = monthsRef.current;
|
|
123
|
+
if (!viewport || !list) return;
|
|
124
|
+
const resize = ()=>syncFromCalendar();
|
|
125
|
+
if ("u" > typeof ResizeObserver) {
|
|
126
|
+
const observer = new ResizeObserver(resize);
|
|
127
|
+
observer.observe(viewport);
|
|
128
|
+
observer.observe(list);
|
|
129
|
+
return ()=>observer.disconnect();
|
|
130
|
+
}
|
|
131
|
+
window.addEventListener('resize', resize);
|
|
132
|
+
return ()=>window.removeEventListener('resize', resize);
|
|
133
|
+
}, [
|
|
134
|
+
calendarStart,
|
|
135
|
+
monthStart
|
|
136
|
+
]);
|
|
137
|
+
function recycleCalendar(geometry) {
|
|
138
|
+
const viewport = calendarRef.current;
|
|
139
|
+
if (!viewport) return;
|
|
140
|
+
const first = positionAt(geometry, viewport.scrollTop);
|
|
141
|
+
const last = positionAt(geometry, viewport.scrollTop + viewport.clientHeight);
|
|
142
|
+
if (!first || !last) return;
|
|
143
|
+
let start = calendarStart;
|
|
144
|
+
if (first.month <= calendarStart + 1 && calendarStart > 0) start = Math.max(0, calendarStart - 4);
|
|
145
|
+
else if (last.month >= calendarStart + CALENDAR_BUFFER - 2 && calendarStart + CALENDAR_BUFFER <= LAST_MONTH) start = Math.min(LAST_MONTH - CALENDAR_BUFFER + 1, calendarStart + 4);
|
|
146
|
+
if (start !== calendarStart) {
|
|
147
|
+
pendingPosition.current = first;
|
|
148
|
+
setCalendarStart(start);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function calendarScroll() {
|
|
152
|
+
const viewport = calendarRef.current;
|
|
153
|
+
if (!viewport || viewport.scrollTop === calendarTop.current) return;
|
|
154
|
+
cancelPendingSeek();
|
|
155
|
+
const geometry = readGeometry();
|
|
156
|
+
if (!geometry) return;
|
|
157
|
+
syncFromCalendar(geometry);
|
|
158
|
+
recycleCalendar(geometry.calendar);
|
|
159
|
+
}
|
|
160
|
+
function monthsScroll() {
|
|
161
|
+
const list = monthsRef.current;
|
|
162
|
+
const viewport = calendarRef.current;
|
|
163
|
+
if (!list || !viewport) return;
|
|
164
|
+
const delta = list.scrollTop - monthsTop.current;
|
|
165
|
+
monthsTop.current = list.scrollTop;
|
|
166
|
+
if (!delta) return;
|
|
167
|
+
const geometry = readGeometry();
|
|
168
|
+
if (!geometry) return;
|
|
169
|
+
if (monthAnchor.current) monthAnchor.current.offset -= delta;
|
|
170
|
+
const base = pendingPosition.current ? offsetFor(geometry.months, pendingPosition.current) ?? windowTop.current : windowTop.current;
|
|
171
|
+
const target = positionAt(geometry.months, base + delta);
|
|
172
|
+
if (target) {
|
|
173
|
+
const offset = offsetFor(geometry.calendar, target);
|
|
174
|
+
const targetStart = calendarStartFor(target.month);
|
|
175
|
+
if (null === offset || offset + viewport.clientHeight > viewport.scrollHeight && targetStart !== calendarStart) {
|
|
176
|
+
pendingPosition.current = target;
|
|
177
|
+
setCalendarStart(targetStart);
|
|
178
|
+
} else {
|
|
179
|
+
cancelPendingSeek();
|
|
180
|
+
setScroll(viewport, offset);
|
|
181
|
+
syncFromCalendar(geometry);
|
|
182
|
+
recycleCalendar(geometry.calendar);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let start = monthStart;
|
|
186
|
+
if (list.scrollTop < 72 && monthStart > 0) start = Math.max(0, monthStart - 12);
|
|
187
|
+
else if (list.scrollTop + list.clientHeight >= list.scrollHeight - 72 && monthStart + MONTH_BUFFER <= LAST_MONTH) start = Math.min(LAST_MONTH - MONTH_BUFFER + 1, monthStart + 12);
|
|
188
|
+
if (start === monthStart) return;
|
|
189
|
+
const anchor = geometry.months.find((row)=>row.month === Math.max(start, monthStart));
|
|
190
|
+
if (anchor) monthAnchor.current = {
|
|
191
|
+
month: anchor.month,
|
|
192
|
+
offset: anchor.start - list.scrollTop
|
|
193
|
+
};
|
|
194
|
+
setMonthStart(start);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
calendarRef,
|
|
198
|
+
monthsRef,
|
|
199
|
+
windowRef,
|
|
200
|
+
registerCalendarMonth (monthIndex, element) {
|
|
201
|
+
if (element) calendarElements.current.set(monthIndex, element);
|
|
202
|
+
else calendarElements.current.delete(monthIndex);
|
|
203
|
+
},
|
|
204
|
+
registerMonth (monthIndex, element) {
|
|
205
|
+
if (element) monthElements.current.set(monthIndex, element);
|
|
206
|
+
else monthElements.current.delete(monthIndex);
|
|
207
|
+
},
|
|
208
|
+
visible,
|
|
209
|
+
calendarScroll,
|
|
210
|
+
monthsScroll,
|
|
211
|
+
calendars: Array.from({
|
|
212
|
+
length: CALENDAR_BUFFER
|
|
213
|
+
}, (_, index)=>calendarStart + index),
|
|
214
|
+
months: Array.from({
|
|
215
|
+
length: MONTH_BUFFER
|
|
216
|
+
}, (_, index)=>monthStart + index)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export { useRangeScroll };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "./index_module.css";
|
|
2
|
+
const index_module = {
|
|
3
|
+
component: "component-XIM8MH",
|
|
4
|
+
block: "block-bqbbVY",
|
|
5
|
+
secondary: "secondary-PcSgFC",
|
|
6
|
+
readOnly: "readOnly-h13qVH",
|
|
7
|
+
error: "error-c8nacQ",
|
|
8
|
+
disabled: "disabled-khgRiN",
|
|
9
|
+
"size-40": "size-40-h9qqeQ",
|
|
10
|
+
size40: "size-40-h9qqeQ",
|
|
11
|
+
"size-48": "size-48-aaoiXa",
|
|
12
|
+
size48: "size-48-aaoiXa",
|
|
13
|
+
"size-56": "size-56-OGJpdt",
|
|
14
|
+
size56: "size-56-OGJpdt",
|
|
15
|
+
"size-64": "size-64-kBX3a2",
|
|
16
|
+
size64: "size-64-kBX3a2",
|
|
17
|
+
"size-72": "size-72-BC6gqa",
|
|
18
|
+
size72: "size-72-BC6gqa",
|
|
19
|
+
field: "field-fNIrel",
|
|
20
|
+
hasOutline: "hasOutline-HAFy7G",
|
|
21
|
+
open: "open-S1x_K5",
|
|
22
|
+
trigger: "trigger-yqmoik",
|
|
23
|
+
valueColumn: "valueColumn-yqimvV",
|
|
24
|
+
value: "value-AslTvH",
|
|
25
|
+
placeholder: "placeholder-kZMY22",
|
|
26
|
+
label: "label-EQek4i",
|
|
27
|
+
innerLabel: "innerLabel-qjRhOo",
|
|
28
|
+
labelRaised: "labelRaised-X1UZfx",
|
|
29
|
+
hasInnerLabel: "hasInnerLabel-KmtJKD",
|
|
30
|
+
icon: "icon-QZ2qDs",
|
|
31
|
+
clear: "clear-Rcrq1X",
|
|
32
|
+
message: "message-IkNTrK",
|
|
33
|
+
popup: "popup-a9mcwf",
|
|
34
|
+
panelContainer: "panelContainer-xno_kL",
|
|
35
|
+
rangePopup: "rangePopup-AVJC5u"
|
|
36
|
+
};
|
|
37
|
+
export default index_module;
|