@xh/hoist 87.1.0 → 87.2.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/.npmignore +39 -0
- package/CHANGELOG.md +99 -20
- package/admin/AdminUtils.ts +64 -0
- package/admin/App.scss +88 -0
- package/admin/columns/UserData.ts +30 -3
- package/admin/detail/RestDetailModel.ts +64 -0
- package/admin/detail/RestDetailPanel.ts +106 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
- package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
- package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
- package/admin/tabs/general/config/ConfigPanel.ts +41 -53
- package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
- package/admin/tabs/general/config/ConfigValue.scss +14 -0
- package/admin/tabs/general/config/ConfigValue.ts +104 -65
- package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
- package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
- package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
- package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
- package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
- package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
- package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
- package/appcontainer/AboutDialogModel.ts +1 -0
- package/appcontainer/AppContainerModel.ts +3 -0
- package/appcontainer/AppStateModel.ts +1 -0
- package/appcontainer/BannerSourceModel.ts +1 -0
- package/appcontainer/ChangelogDialogModel.ts +1 -0
- package/appcontainer/ColChooserOptionsModel.ts +1 -0
- package/appcontainer/ExceptionDialogModel.ts +1 -0
- package/appcontainer/FeedbackDialogModel.ts +1 -0
- package/appcontainer/ImpersonationBarModel.ts +1 -0
- package/appcontainer/MessageSourceModel.ts +1 -0
- package/appcontainer/OptionsDialogModel.ts +1 -0
- package/appcontainer/PageStateModel.ts +1 -0
- package/appcontainer/RouterModel.ts +2 -0
- package/appcontainer/SizingModeModel.ts +1 -0
- package/appcontainer/ThemeModel.ts +1 -0
- package/appcontainer/ToastSourceModel.ts +1 -0
- package/appcontainer/UserAgentModel.ts +1 -0
- package/appcontainer/ViewportSizeModel.ts +1 -0
- package/build/types/admin/AdminUtils.d.ts +29 -0
- package/build/types/admin/columns/UserData.d.ts +3 -0
- package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
- package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
- package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
- package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
- package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
- package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
- package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
- package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
- package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
- package/build/types/appcontainer/AppStateModel.d.ts +1 -0
- package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
- package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
- package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
- package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
- package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
- package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
- package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
- package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
- package/build/types/appcontainer/PageStateModel.d.ts +1 -0
- package/build/types/appcontainer/RouterModel.d.ts +1 -0
- package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
- package/build/types/appcontainer/ThemeModel.d.ts +1 -0
- package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
- package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
- package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
- package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
- package/build/types/cmp/card/Card.d.ts +2 -2
- package/build/types/cmp/chart/ChartModel.d.ts +2 -0
- package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
- package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
- package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
- package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
- package/build/types/cmp/daterange/Types.d.ts +152 -0
- package/build/types/cmp/daterange/index.d.ts +4 -0
- package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
- package/build/types/cmp/form/FormModel.d.ts +3 -1
- package/build/types/cmp/grid/GridModel.d.ts +2 -0
- package/build/types/cmp/grid/Types.d.ts +2 -0
- package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
- package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
- package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
- package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
- package/build/types/cmp/tab/TabModel.d.ts +3 -1
- package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
- package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
- package/build/types/core/HoistBase.d.ts +7 -0
- package/build/types/core/HoistProps.d.ts +26 -1
- package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
- package/build/types/core/types/Telemetry.d.ts +1 -1
- package/build/types/data/Store.d.ts +3 -1
- package/build/types/data/StoreSelectionModel.d.ts +3 -1
- package/build/types/data/cube/Cube.d.ts +5 -2
- package/build/types/data/cube/View.d.ts +2 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
- package/build/types/desktop/cmp/button/Button.d.ts +2 -2
- package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
- package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
- package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
- package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
- package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
- package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
- package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
- package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
- package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
- package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
- package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
- package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
- package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
- package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
- package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
- package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
- package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
- package/build/types/desktop/cmp/input/index.d.ts +1 -0
- package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
- package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
- package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
- package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/icon/Icon.d.ts +3 -0
- package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
- package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
- package/build/types/kit/react-day-picker/index.d.ts +3 -3
- package/build/types/mobile/cmp/button/Button.d.ts +2 -2
- package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
- package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
- package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
- package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
- package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +3 -1
- package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
- package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/svc/InspectorService.d.ts +1 -0
- package/build/types/utils/js/LangUtils.d.ts +24 -5
- package/cmp/README.md +1 -0
- package/cmp/ag-grid/AgGridModel.ts +5 -0
- package/cmp/badge/Badge.ts +5 -3
- package/cmp/card/Card.ts +4 -3
- package/cmp/chart/ChartModel.ts +11 -1
- package/cmp/dataview/DataViewModel.ts +7 -1
- package/cmp/daterange/DateRangePickerModel.ts +628 -0
- package/cmp/daterange/DateRangePresets.ts +274 -0
- package/cmp/daterange/DateRangeUtils.ts +557 -0
- package/cmp/daterange/README.md +434 -0
- package/cmp/daterange/Types.ts +203 -0
- package/cmp/daterange/index.ts +10 -0
- package/cmp/filter/FilterChooserModel.ts +7 -4
- package/cmp/form/FormModel.ts +6 -0
- package/cmp/form/README.md +1 -0
- package/cmp/grid/GridModel.ts +24 -4
- package/cmp/grid/Types.ts +3 -0
- package/cmp/grid/filter/GridFilterModel.ts +3 -1
- package/cmp/grid/impl/RecordSortUtils.ts +10 -2
- package/cmp/grouping/GroupingChooserModel.ts +6 -1
- package/cmp/input/HoistInputProps.ts +50 -2
- package/cmp/input/README.md +42 -0
- package/cmp/layout/Box.ts +3 -2
- package/cmp/tab/TabContainerModel.ts +9 -1
- package/cmp/tab/TabModel.ts +5 -0
- package/cmp/treemap/SplitTreeMapModel.ts +12 -2
- package/cmp/treemap/TreeMapModel.ts +6 -1
- package/cmp/viewmanager/ViewManagerModel.ts +6 -1
- package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
- package/core/HoistBase.ts +11 -0
- package/core/HoistProps.ts +27 -0
- package/core/README.md +46 -0
- package/core/impl/InstallServices.ts +5 -1
- package/core/model/RootRefreshContextModel.ts +2 -0
- package/core/types/Telemetry.ts +1 -1
- package/data/README.md +3 -1
- package/data/Store.ts +7 -0
- package/data/StoreSelectionModel.ts +5 -1
- package/data/cube/Cube.ts +12 -3
- package/data/cube/View.ts +5 -1
- package/desktop/README.md +2 -0
- package/desktop/appcontainer/LoginPanel.ts +2 -0
- package/desktop/cmp/button/Button.scss +71 -42
- package/desktop/cmp/button/Button.ts +10 -1
- package/desktop/cmp/button/ButtonGroup.ts +4 -1
- package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
- package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
- package/desktop/cmp/dash/DashConfig.ts +3 -0
- package/desktop/cmp/dash/DashViewModel.ts +14 -1
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
- package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
- package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
- package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
- package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
- package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
- package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
- package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
- package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
- package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
- package/desktop/cmp/daterange/index.ts +8 -0
- package/desktop/cmp/dock/DockContainerModel.ts +6 -1
- package/desktop/cmp/dock/DockViewModel.ts +6 -1
- package/desktop/cmp/filechooser/FileChooser.ts +4 -0
- package/desktop/cmp/form/FormField.ts +1 -0
- package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
- package/desktop/cmp/input/Checkbox.ts +1 -0
- package/desktop/cmp/input/CodeInput.ts +1 -0
- package/desktop/cmp/input/DateInput.scss +1 -1
- package/desktop/cmp/input/DateInput.ts +1 -0
- package/desktop/cmp/input/IntentInput.scss +202 -0
- package/desktop/cmp/input/IntentInput.ts +213 -0
- package/desktop/cmp/input/NumberInput.ts +11 -2
- package/desktop/cmp/input/Picker.ts +1 -0
- package/desktop/cmp/input/RadioInput.ts +2 -1
- package/desktop/cmp/input/SegmentedControl.scss +45 -2
- package/desktop/cmp/input/SegmentedControl.ts +66 -11
- package/desktop/cmp/input/Select.ts +9 -7
- package/desktop/cmp/input/Slider.ts +2 -0
- package/desktop/cmp/input/SwitchInput.ts +1 -0
- package/desktop/cmp/input/TextArea.ts +11 -2
- package/desktop/cmp/input/TextInput.ts +11 -2
- package/desktop/cmp/input/index.ts +1 -0
- package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
- package/desktop/cmp/panel/Panel.ts +8 -5
- package/desktop/cmp/panel/PanelModel.ts +8 -0
- package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
- package/desktop/cmp/rest/RestGridModel.ts +4 -0
- package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
- package/docs/README.md +2 -0
- package/docs/build-and-publish.md +23 -7
- package/docs/doc-registry.json +8 -0
- package/docs/version-compatibility.md +2 -0
- package/icon/Icon.ts +9 -0
- package/icon/index.ts +24 -0
- package/inspector/README.md +8 -2
- package/inspector/impl/InspectorUtils.ts +12 -0
- package/inspector/instances/DiagnosticsModel.ts +3 -2
- package/inspector/instances/InstancesModel.ts +81 -14
- package/inspector/instances/InstancesPanel.ts +6 -0
- package/kit/react-day-picker/index.ts +3 -3
- package/mobile/appcontainer/LoginPanel.ts +2 -0
- package/mobile/cmp/button/Button.ts +16 -3
- package/mobile/cmp/form/FormField.ts +1 -0
- package/mobile/cmp/input/Checkbox.ts +1 -0
- package/mobile/cmp/input/DateInput.ts +1 -0
- package/mobile/cmp/input/Label.ts +13 -8
- package/mobile/cmp/input/NumberInput.ts +11 -2
- package/mobile/cmp/input/SearchInput.ts +1 -0
- package/mobile/cmp/input/SegmentedControl.scss +36 -6
- package/mobile/cmp/input/SegmentedControl.ts +37 -11
- package/mobile/cmp/input/Select.ts +2 -0
- package/mobile/cmp/input/SwitchInput.ts +1 -0
- package/mobile/cmp/input/TextArea.ts +11 -2
- package/mobile/cmp/input/TextInput.ts +11 -2
- package/mobile/cmp/navigator/NavigatorModel.ts +6 -1
- package/mobile/cmp/navigator/PageModel.ts +6 -1
- package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
- package/package.json +19 -19
- package/styles/vars.scss +32 -2
- package/svc/InspectorService.ts +2 -0
- package/svc/TraceService.ts +5 -2
- package/utils/js/LangUtils.ts +37 -5
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
8
|
+
import {throwIf} from '@xh/hoist/utils/js';
|
|
9
|
+
import {isFinite, isFunction, isPlainObject, isString} from 'lodash';
|
|
10
|
+
import type {
|
|
11
|
+
DateRangeContext,
|
|
12
|
+
DateRangeFormat,
|
|
13
|
+
DateRangePickerTab,
|
|
14
|
+
DateRangePreset,
|
|
15
|
+
DateRangeSelection,
|
|
16
|
+
DateRangeUnit,
|
|
17
|
+
LocalDateRange,
|
|
18
|
+
MonthDateRangeSelection,
|
|
19
|
+
PresetDateRangeSelection,
|
|
20
|
+
QuarterDateRangeSelection,
|
|
21
|
+
RelativeDateRangeSelection,
|
|
22
|
+
ResolvedDateRange,
|
|
23
|
+
YearDateRangeSelection
|
|
24
|
+
} from './Types';
|
|
25
|
+
|
|
26
|
+
/** All picker tabs, in their default display order. */
|
|
27
|
+
export const DATE_RANGE_PICKER_TABS: DateRangePickerTab[] = [
|
|
28
|
+
'presets',
|
|
29
|
+
'relative',
|
|
30
|
+
'period',
|
|
31
|
+
'custom'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/** Units offered on the Relative tab's unit control, in display order. */
|
|
35
|
+
export const DATE_RANGE_UNITS: DateRangeUnit[] = ['days', 'weeks', 'months', 'quarters', 'years'];
|
|
36
|
+
|
|
37
|
+
/** Largest `count` accepted for a relative selection. */
|
|
38
|
+
export const MAX_RELATIVE_COUNT = 999;
|
|
39
|
+
|
|
40
|
+
/** Furthest a preset or relative selection can be stepped from its natural range, either way. */
|
|
41
|
+
export const MAX_STEP_OFFSET = 9999;
|
|
42
|
+
|
|
43
|
+
/** The calendar unit of each pinned-period selection kind. */
|
|
44
|
+
const PERIOD_UNITS = {month: 'months', quarter: 'quarters', year: 'years'} as const;
|
|
45
|
+
|
|
46
|
+
/** Year bounds for month and year selections - shared by validation and picker navigation. */
|
|
47
|
+
export const MIN_SELECTION_YEAR = 1900;
|
|
48
|
+
export const MAX_SELECTION_YEAR = 9999;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a selection to its current range, and to the comparable range immediately before it.
|
|
52
|
+
* Throws if the selection is a preset unknown to the context - validate with
|
|
53
|
+
* {@link parseDateRangeSelection} first for untrusted input.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveDateRange(
|
|
56
|
+
sel: DateRangeSelection,
|
|
57
|
+
ctx: DateRangeContext
|
|
58
|
+
): ResolvedDateRange {
|
|
59
|
+
const {anchorDate, minDate, maxDate} = ctx;
|
|
60
|
+
|
|
61
|
+
switch (sel.kind) {
|
|
62
|
+
case 'preset': {
|
|
63
|
+
const preset = getPreset(sel.token, ctx);
|
|
64
|
+
return applyOffset(
|
|
65
|
+
preset.resolve(ctx),
|
|
66
|
+
range => resolvePresetPrior(preset, range, ctx),
|
|
67
|
+
range => resolvePresetNext(preset, range, ctx),
|
|
68
|
+
sel.offset
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'relative': {
|
|
73
|
+
const {unit} = sel,
|
|
74
|
+
count = Math.max(1, sel.count),
|
|
75
|
+
// Days need no special case: a day is already a calendar boundary, so both
|
|
76
|
+
// expressions below reduce to `anchorDate - count + 1` for that unit.
|
|
77
|
+
start = sel.snap
|
|
78
|
+
? // Calendar-aligned - the current (partial) unit counts as one.
|
|
79
|
+
anchorDate.startOf(unit).subtract(count - 1, unit)
|
|
80
|
+
: // Rolling window of exactly `count` units ending on the anchor date.
|
|
81
|
+
anchorDate.subtract(count, unit).nextDay();
|
|
82
|
+
return applyOffset(
|
|
83
|
+
{start, end: anchorDate},
|
|
84
|
+
range => shiftRelative(sel, range, -1, ctx),
|
|
85
|
+
range => shiftRelative(sel, range, 1, ctx),
|
|
86
|
+
sel.offset
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'month':
|
|
91
|
+
case 'quarter':
|
|
92
|
+
case 'year':
|
|
93
|
+
return resolveCalendarUnit(periodStart(sel), PERIOD_UNITS[sel.kind], minDate, maxDate);
|
|
94
|
+
|
|
95
|
+
case 'custom': {
|
|
96
|
+
const current = {start: LocalDate.get(sel.start), end: LocalDate.get(sel.end)};
|
|
97
|
+
return {current, prior: shiftByDuration(current, -1)};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Step a natural range `|offset|` periods - back via its prior-range logic, forward via its
|
|
104
|
+
* next-range logic - then compute the prior of where it lands. Stops early if the range becomes
|
|
105
|
+
* unbounded; `stepDateRangeSelection` refuses offsets the range cannot reach.
|
|
106
|
+
*/
|
|
107
|
+
function applyOffset(
|
|
108
|
+
current: LocalDateRange,
|
|
109
|
+
priorFn: (range: LocalDateRange) => LocalDateRange | null,
|
|
110
|
+
nextFn: (range: LocalDateRange) => LocalDateRange | null,
|
|
111
|
+
offset: number = 0
|
|
112
|
+
): ResolvedDateRange {
|
|
113
|
+
const stepFn = offset < 0 ? priorFn : nextFn;
|
|
114
|
+
for (let i = 0; i < Math.abs(offset); i++) {
|
|
115
|
+
const stepped = stepFn(current);
|
|
116
|
+
if (!stepped) break;
|
|
117
|
+
current = stepped;
|
|
118
|
+
}
|
|
119
|
+
return {current, prior: priorFn(current)};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolvePresetPrior(
|
|
123
|
+
preset: DateRangePreset,
|
|
124
|
+
range: LocalDateRange,
|
|
125
|
+
ctx: DateRangeContext
|
|
126
|
+
): LocalDateRange | null {
|
|
127
|
+
return preset.resolvePrior ? preset.resolvePrior(range, ctx) : shiftByDuration(range, -1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolvePresetNext(
|
|
131
|
+
preset: DateRangePreset,
|
|
132
|
+
range: LocalDateRange,
|
|
133
|
+
ctx: DateRangeContext
|
|
134
|
+
): LocalDateRange | null {
|
|
135
|
+
return preset.resolveNext ? preset.resolveNext(range, ctx) : shiftByDuration(range, 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compare like against like: a lookback in weeks or larger units steps by the same units,
|
|
140
|
+
* matching the equivalent presets - so 3 months ending May 31 compares against 3 months ending
|
|
141
|
+
* Feb 28, not 92 days. Days keep an equal number of days, with a single day walking by business
|
|
142
|
+
* day when the model is in `businessDayMode`. `dir` is -1 for the prior range, 1 for the next.
|
|
143
|
+
*/
|
|
144
|
+
function shiftRelative(
|
|
145
|
+
sel: RelativeDateRangeSelection,
|
|
146
|
+
{start, end}: LocalDateRange,
|
|
147
|
+
dir: 1 | -1,
|
|
148
|
+
ctx: DateRangeContext
|
|
149
|
+
): LocalDateRange {
|
|
150
|
+
const {unit} = sel,
|
|
151
|
+
count = Math.max(1, sel.count) * dir;
|
|
152
|
+
if (unit !== 'days') return {start: start.add(count, unit), end: end.add(count, unit)};
|
|
153
|
+
if (Math.abs(count) === 1 && ctx.businessDayMode) {
|
|
154
|
+
return singleDay(dir < 0 ? previousBusinessDay(start, ctx) : nextBusinessDay(start, ctx));
|
|
155
|
+
}
|
|
156
|
+
return shiftByDuration({start, end}, dir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A full calendar month, quarter or year, clamped to `maxDate` (and `minDate`) when those fall
|
|
161
|
+
* within it - so the current month covers the 1st through the anchor. A period entirely beyond
|
|
162
|
+
* `maxDate` (e.g. a persisted pick from a user with a later anchor) keeps its natural bounds
|
|
163
|
+
* rather than producing an inverted range.
|
|
164
|
+
*/
|
|
165
|
+
function resolveCalendarUnit(
|
|
166
|
+
naturalStart: LocalDate,
|
|
167
|
+
unit: PeriodUnit,
|
|
168
|
+
minDate: LocalDate,
|
|
169
|
+
maxDate: LocalDate
|
|
170
|
+
): ResolvedDateRange {
|
|
171
|
+
const naturalEnd = naturalStart.endOf(unit),
|
|
172
|
+
start = minDate && minDate > naturalStart && minDate <= naturalEnd ? minDate : naturalStart,
|
|
173
|
+
end = maxDate && maxDate >= naturalStart && maxDate < naturalEnd ? maxDate : naturalEnd,
|
|
174
|
+
priorStart = naturalStart.subtract(1, unit);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
current: {start, end},
|
|
178
|
+
prior:
|
|
179
|
+
end === naturalEnd
|
|
180
|
+
? {start: priorStart, end: priorStart.endOf(unit)}
|
|
181
|
+
: // Clamped to a partial period - compare against the same span one unit earlier.
|
|
182
|
+
{start: priorStart, end: end.subtract(1, unit)}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The adjacent range of equal duration in days - preceding for `dir` -1, following for 1 - or
|
|
188
|
+
* null if `current` is unbounded.
|
|
189
|
+
*/
|
|
190
|
+
function shiftByDuration({start, end}: LocalDateRange, dir: 1 | -1): LocalDateRange | null {
|
|
191
|
+
if (!start || !end) return null;
|
|
192
|
+
const days = (end.diff(start, 'days') + 1) * dir;
|
|
193
|
+
return {start: start.add(days, 'days'), end: end.add(days, 'days')};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A range of the one given day. */
|
|
197
|
+
export function singleDay(day: LocalDate): LocalDateRange {
|
|
198
|
+
return {start: day, end: day};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The nearest business day strictly before `date`, per the context's `isBusinessDay`. Bounded, to
|
|
203
|
+
* guard against a test that never returns true.
|
|
204
|
+
*/
|
|
205
|
+
export function previousBusinessDay(date: LocalDate, ctx: BusinessDayContext): LocalDate {
|
|
206
|
+
let ret = date.previousDay();
|
|
207
|
+
for (let i = 0; i < 366 && !ctx.isBusinessDay(ret); i++) ret = ret.previousDay();
|
|
208
|
+
return ret;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The nearest business day strictly after `date`, per the context's `isBusinessDay`. Bounded. */
|
|
212
|
+
export function nextBusinessDay(date: LocalDate, ctx: BusinessDayContext): LocalDate {
|
|
213
|
+
let ret = date.nextDay();
|
|
214
|
+
for (let i = 0; i < 366 && !ctx.isBusinessDay(ret); i++) ret = ret.nextDay();
|
|
215
|
+
return ret;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** `date` itself if it is a business day, else the nearest business day before it. */
|
|
219
|
+
export function businessDayOnOrBefore(date: LocalDate, ctx: BusinessDayContext): LocalDate {
|
|
220
|
+
return ctx.isBusinessDay(date) ? date : previousBusinessDay(date, ctx);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type BusinessDayContext = Pick<DateRangeContext, 'isBusinessDay'>;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The day before `date` - the previous business day in `businessDayMode`, else the previous
|
|
227
|
+
* calendar day. The step taken by single-day presets and their prior ranges.
|
|
228
|
+
*/
|
|
229
|
+
export function previousDayInMode(date: LocalDate, ctx: DateRangeContext): LocalDate {
|
|
230
|
+
return ctx.businessDayMode ? previousBusinessDay(date, ctx) : date.previousDay();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The day after `date` - the next business day in `businessDayMode`, else the next calendar day. */
|
|
234
|
+
export function nextDayInMode(date: LocalDate, ctx: DateRangeContext): LocalDate {
|
|
235
|
+
return ctx.businessDayMode ? nextBusinessDay(date, ctx) : date.nextDay();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validate and normalize a raw (e.g. persisted or app-supplied) value into a
|
|
240
|
+
* {@link DateRangeSelection}. Accepts a bare preset token string as shorthand for a preset
|
|
241
|
+
* selection. Returns null for anything unrecognized, out of bounds, or naming a preset not in
|
|
242
|
+
* `presets` - callers should substitute their default.
|
|
243
|
+
*/
|
|
244
|
+
export function parseDateRangeSelection(
|
|
245
|
+
raw: unknown,
|
|
246
|
+
presets: Record<string, DateRangePreset>
|
|
247
|
+
): DateRangeSelection | null {
|
|
248
|
+
if (isString(raw)) return presets[raw] ? {kind: 'preset', token: raw} : null;
|
|
249
|
+
if (!isPlainObject(raw)) return null;
|
|
250
|
+
|
|
251
|
+
const obj = raw as any;
|
|
252
|
+
switch (obj.kind) {
|
|
253
|
+
case 'preset': {
|
|
254
|
+
const offset = parseOffset(obj.offset);
|
|
255
|
+
return isString(obj.token) && presets[obj.token] && offset != null
|
|
256
|
+
? withOffset({kind: 'preset', token: obj.token}, offset)
|
|
257
|
+
: null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case 'relative': {
|
|
261
|
+
const count = Math.round(obj.count),
|
|
262
|
+
offset = parseOffset(obj.offset),
|
|
263
|
+
{unit} = obj;
|
|
264
|
+
return isFinite(count) &&
|
|
265
|
+
count >= 1 &&
|
|
266
|
+
count <= MAX_RELATIVE_COUNT &&
|
|
267
|
+
DATE_RANGE_UNITS.includes(unit) &&
|
|
268
|
+
offset != null
|
|
269
|
+
? // Snap has no meaning at day grain - normalize it away so values compare equal.
|
|
270
|
+
withOffset(
|
|
271
|
+
{kind: 'relative', count, unit, snap: unit !== 'days' && obj.snap === true},
|
|
272
|
+
offset
|
|
273
|
+
)
|
|
274
|
+
: null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'month': {
|
|
278
|
+
const {year, month} = obj;
|
|
279
|
+
return isValidYear(year) && Number.isInteger(month) && month >= 1 && month <= 12
|
|
280
|
+
? {kind: 'month', year, month}
|
|
281
|
+
: null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'quarter': {
|
|
285
|
+
const {year, quarter} = obj;
|
|
286
|
+
return isValidYear(year) && Number.isInteger(quarter) && quarter >= 1 && quarter <= 4
|
|
287
|
+
? {kind: 'quarter', year, quarter}
|
|
288
|
+
: null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'year':
|
|
292
|
+
return isValidYear(obj.year) ? {kind: 'year', year: obj.year} : null;
|
|
293
|
+
|
|
294
|
+
case 'custom': {
|
|
295
|
+
const start = parseIsoDate(obj.start),
|
|
296
|
+
end = parseIsoDate(obj.end);
|
|
297
|
+
if (!start || !end) return null;
|
|
298
|
+
return start <= end
|
|
299
|
+
? {kind: 'custom', start: start.isoString, end: end.isoString}
|
|
300
|
+
: {kind: 'custom', start: end.isoString, end: start.isoString};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
default:
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** A missing offset is zero. Anything else must be an integer within ±MAX_STEP_OFFSET. */
|
|
309
|
+
function parseOffset(raw: unknown): number | null {
|
|
310
|
+
if (raw == null) return 0;
|
|
311
|
+
return Number.isInteger(raw) && Math.abs(raw as number) <= MAX_STEP_OFFSET
|
|
312
|
+
? (raw as number)
|
|
313
|
+
: null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The selection at the given offset, in normalized form - `offset` present only when non-zero. */
|
|
317
|
+
function withOffset<T extends PresetDateRangeSelection | RelativeDateRangeSelection>(
|
|
318
|
+
sel: T,
|
|
319
|
+
offset: number
|
|
320
|
+
): T {
|
|
321
|
+
const {offset: _, ...rest} = sel;
|
|
322
|
+
return (offset ? {...rest, offset} : rest) as T;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isValidYear(year: unknown): year is number {
|
|
326
|
+
return (
|
|
327
|
+
Number.isInteger(year) &&
|
|
328
|
+
(year as number) >= MIN_SELECTION_YEAR &&
|
|
329
|
+
(year as number) <= MAX_SELECTION_YEAR
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseIsoDate(s: unknown): LocalDate | null {
|
|
334
|
+
if (!isString(s) || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
|
|
335
|
+
try {
|
|
336
|
+
return LocalDate.get(s);
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Short label for a selection, suitable for the picker trigger - e.g. `MTD`, `Prev 6 Months`,
|
|
344
|
+
* `Aug 2026`, `2025`, or `Custom`.
|
|
345
|
+
*
|
|
346
|
+
* Month and year picks read as the period they name - `Sep 2026`, `2026` - even when clamped to
|
|
347
|
+
* the anchor date, with the trigger's dates showing the clamp. `MTD` and `YTD` are reserved for
|
|
348
|
+
* the presets, which are live and step to the same partial span a unit earlier, where a pinned
|
|
349
|
+
* month or year steps by whole units. The `prevMonth` and `prevYear` presets read as the period
|
|
350
|
+
* too, since they resolve to a full one.
|
|
351
|
+
*
|
|
352
|
+
* Once stepped to a non-zero offset, the trigger's dates locate the range and the label describes
|
|
353
|
+
* only its shape: a rolling window reads as its length (`7 Days`, `3 Months`), a named period as
|
|
354
|
+
* the period (`Jul 2026`), and a to-date preset as its name with the signed offset (`MTD −1`,
|
|
355
|
+
* `MTD +1`).
|
|
356
|
+
*/
|
|
357
|
+
export function getDateRangeLabel(sel: DateRangeSelection, ctx: DateRangeContext): string {
|
|
358
|
+
switch (sel.kind) {
|
|
359
|
+
case 'preset': {
|
|
360
|
+
const preset = getPreset(sel.token, ctx),
|
|
361
|
+
{offset} = sel;
|
|
362
|
+
if (!offset) return evalLabel(preset.label, ctx);
|
|
363
|
+
return preset.shiftedLabel
|
|
364
|
+
? preset.shiftedLabel(resolveDateRange(sel, ctx).current, offset, ctx)
|
|
365
|
+
: `${evalLabel(preset.label, ctx)} ${fmtDateRangeOffset(offset)}`;
|
|
366
|
+
}
|
|
367
|
+
case 'relative': {
|
|
368
|
+
const length = `${sel.count} ${getDateRangeUnitLabel(sel.unit, sel.count)}`;
|
|
369
|
+
return sel.offset ? length : `Prev ${length}`;
|
|
370
|
+
}
|
|
371
|
+
case 'month':
|
|
372
|
+
return getMonthStart(sel.year, sel.month).format('MMM YYYY');
|
|
373
|
+
case 'quarter':
|
|
374
|
+
return `Q${sel.quarter} ${sel.year}`;
|
|
375
|
+
case 'year':
|
|
376
|
+
return String(sel.year);
|
|
377
|
+
case 'custom':
|
|
378
|
+
return 'Custom';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Display name for a preset's row within the picker - its `name`, falling back to its `label`. */
|
|
383
|
+
export function getDateRangePresetName(preset: DateRangePreset, ctx: DateRangeContext): string {
|
|
384
|
+
return evalLabel(preset.name ?? preset.label, ctx);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function evalLabel(label: DateRangePreset['label'], ctx: DateRangeContext): string {
|
|
388
|
+
return isFunction(label) ? label(ctx) : label;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A signed offset as `−n` or `+n`, with a true minus sign - the default suffix for a stepped label. */
|
|
392
|
+
export function fmtDateRangeOffset(offset: number): string {
|
|
393
|
+
return `${offset < 0 ? '−' : '+'}${Math.abs(offset)}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getPreset(token: string, ctx: DateRangeContext): DateRangePreset {
|
|
397
|
+
const preset = ctx.presets[token];
|
|
398
|
+
throwIf(!preset, `Unknown date range preset: '${token}'.`);
|
|
399
|
+
return preset;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const UNIT_LABELS: Record<DateRangeUnit, string> = {
|
|
403
|
+
days: 'Day',
|
|
404
|
+
weeks: 'Week',
|
|
405
|
+
months: 'Month',
|
|
406
|
+
quarters: 'Quarter',
|
|
407
|
+
years: 'Year'
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/** Title-case unit label, singular when count is 1 - e.g. `Days`, `Month`. */
|
|
411
|
+
export function getDateRangeUnitLabel(unit: DateRangeUnit, count: number = 2): string {
|
|
412
|
+
const singular = UNIT_LABELS[unit];
|
|
413
|
+
return count === 1 ? singular : `${singular}s`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Format a date per a {@link DateRangeFormat} - a moment.js format string or a function. */
|
|
417
|
+
export function fmtDate(date: LocalDate, format: DateRangeFormat): string {
|
|
418
|
+
return isFunction(format) ? format(date) : date.format(format);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Format a range as `start ▸ end` per `dateFormat`, with `…` for an unbounded edge. A single-day
|
|
423
|
+
* range formats as that one date per `singleDayFormat`, which defaults to `dateFormat`. Returns an empty
|
|
424
|
+
* string for a null range.
|
|
425
|
+
*/
|
|
426
|
+
export function fmtDateRange(
|
|
427
|
+
range: LocalDateRange,
|
|
428
|
+
dateFormat: DateRangeFormat = 'YYYY-MM-DD',
|
|
429
|
+
singleDayFormat: DateRangeFormat = dateFormat
|
|
430
|
+
): string {
|
|
431
|
+
if (!range) return '';
|
|
432
|
+
const {start, end} = range,
|
|
433
|
+
fmt = (d: LocalDate) => (d ? fmtDate(d, dateFormat) : '…');
|
|
434
|
+
return start && start === end ? fmtDate(start, singleDayFormat) : `${fmt(start)} ▸ ${fmt(end)}`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** First day of the given month (1-12) of the given year. */
|
|
438
|
+
export function getMonthStart(year: number, month: number): LocalDate {
|
|
439
|
+
return LocalDate.get(`${year}-${String(month).padStart(2, '0')}-01`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** First day of the given quarter (1-4) of the given year. */
|
|
443
|
+
export function getQuarterStart(year: number, quarter: number): LocalDate {
|
|
444
|
+
return getMonthStart(year, quarter * 3 - 2);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
type PeriodSelection = MonthDateRangeSelection | QuarterDateRangeSelection | YearDateRangeSelection;
|
|
448
|
+
type PeriodUnit = (typeof PERIOD_UNITS)[PeriodSelection['kind']];
|
|
449
|
+
|
|
450
|
+
/** Natural (unclamped) first day of a month, quarter or year selection. */
|
|
451
|
+
function periodStart(sel: PeriodSelection): LocalDate {
|
|
452
|
+
switch (sel.kind) {
|
|
453
|
+
case 'month':
|
|
454
|
+
return getMonthStart(sel.year, sel.month);
|
|
455
|
+
case 'quarter':
|
|
456
|
+
return getQuarterStart(sel.year, sel.quarter);
|
|
457
|
+
case 'year':
|
|
458
|
+
return LocalDate.get(`${sel.year}-01-01`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Move a selection by `steps` periods - negative to go back, positive to go forward. No selection
|
|
464
|
+
* changes kind:
|
|
465
|
+
*
|
|
466
|
+
* - Preset and relative selections adjust their `offset`, stepping through their own prior- and
|
|
467
|
+
* next-range logic - so a lookback in months steps by months, and a single day steps by
|
|
468
|
+
* business day in `businessDayMode`. Their natural range ends on the anchor date, so a positive
|
|
469
|
+
* offset is reachable only when `maxDate` allows dates beyond it.
|
|
470
|
+
* - Month, quarter and year selections step by calendar unit.
|
|
471
|
+
* - Custom selections step by their length in days - or by business day when a single day in
|
|
472
|
+
* `businessDayMode`.
|
|
473
|
+
*
|
|
474
|
+
* Steps stop at the context's `minDate` and `maxDate`. Returns null when the selection cannot
|
|
475
|
+
* move that way: it is already at a bound, or unbounded.
|
|
476
|
+
*/
|
|
477
|
+
export function stepDateRangeSelection(
|
|
478
|
+
sel: DateRangeSelection,
|
|
479
|
+
steps: number,
|
|
480
|
+
ctx: DateRangeContext
|
|
481
|
+
): DateRangeSelection | null {
|
|
482
|
+
if (!steps) return null;
|
|
483
|
+
const {minDate, maxDate} = ctx;
|
|
484
|
+
|
|
485
|
+
if (sel.kind === 'preset' || sel.kind === 'relative') {
|
|
486
|
+
const offset = sel.offset ?? 0,
|
|
487
|
+
nextOffset = Math.max(-MAX_STEP_OFFSET, Math.min(MAX_STEP_OFFSET, offset + steps));
|
|
488
|
+
if (nextOffset === offset) return null;
|
|
489
|
+
|
|
490
|
+
const next = withOffset(sel, nextOffset),
|
|
491
|
+
{current} = resolveDateRange(next, ctx),
|
|
492
|
+
{current: prev} = resolveDateRange(sel, ctx);
|
|
493
|
+
// Unbounded, or the prior/next logic could not reach the requested offset.
|
|
494
|
+
if (!current.start || !current.end) return null;
|
|
495
|
+
if (current.start === prev.start && current.end === prev.end) return null;
|
|
496
|
+
// A stepped range may not lie entirely outside the bounds.
|
|
497
|
+
if (current.end > maxDate || (minDate && current.start < minDate)) return null;
|
|
498
|
+
return next;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (sel.kind === 'month' || sel.kind === 'quarter' || sel.kind === 'year') {
|
|
502
|
+
const unit = PERIOD_UNITS[sel.kind],
|
|
503
|
+
start = periodStart(sel),
|
|
504
|
+
// Never step beyond the period containing a bound, or below the supported years.
|
|
505
|
+
maxStart = maxDate.startOf(unit),
|
|
506
|
+
minStart = (minDate ?? LocalDate.get(`${MIN_SELECTION_YEAR}-01-01`)).startOf(unit);
|
|
507
|
+
|
|
508
|
+
let next = start.add(steps, unit);
|
|
509
|
+
if (next > maxStart) next = maxStart;
|
|
510
|
+
if (next < minStart) next = minStart;
|
|
511
|
+
// No move, or clamping reversed the direction - e.g. stepping forward from a period
|
|
512
|
+
// already beyond `maxDate`.
|
|
513
|
+
if (steps > 0 ? next <= start : next >= start) return null;
|
|
514
|
+
|
|
515
|
+
const year = next.moment.year();
|
|
516
|
+
switch (sel.kind) {
|
|
517
|
+
case 'month':
|
|
518
|
+
return {kind: 'month', year, month: next.moment.month() + 1};
|
|
519
|
+
case 'quarter':
|
|
520
|
+
return {kind: 'quarter', year, quarter: next.moment.quarter()};
|
|
521
|
+
case 'year':
|
|
522
|
+
return {kind: 'year', year};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const {start, end} = resolveDateRange(sel, ctx).current;
|
|
527
|
+
if (!start || !end) return null;
|
|
528
|
+
|
|
529
|
+
// A single day walks by business day in that mode - stopping at a bound rather than crossing it.
|
|
530
|
+
if (start === end && ctx.businessDayMode) {
|
|
531
|
+
let day = start;
|
|
532
|
+
for (let i = 0; i < Math.abs(steps); i++) {
|
|
533
|
+
day = steps > 0 ? nextBusinessDay(day, ctx) : previousBusinessDay(day, ctx);
|
|
534
|
+
}
|
|
535
|
+
if (day > maxDate || (minDate && day < minDate)) return null;
|
|
536
|
+
return {kind: 'custom', start: day.isoString, end: day.isoString};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const dayCount = end.diff(start, 'days') + 1;
|
|
540
|
+
let nextStart = start.add(dayCount * steps, 'days'),
|
|
541
|
+
nextEnd = end.add(dayCount * steps, 'days');
|
|
542
|
+
|
|
543
|
+
if (nextEnd > maxDate) {
|
|
544
|
+
nextEnd = maxDate;
|
|
545
|
+
nextStart = maxDate.subtract(dayCount - 1, 'days');
|
|
546
|
+
}
|
|
547
|
+
if (minDate && nextStart < minDate) {
|
|
548
|
+
nextStart = minDate;
|
|
549
|
+
nextEnd = minDate.add(dayCount - 1, 'days');
|
|
550
|
+
// A range longer than the bounds themselves - fill them.
|
|
551
|
+
if (nextEnd > maxDate) nextEnd = maxDate;
|
|
552
|
+
}
|
|
553
|
+
// As above - never move against the requested direction.
|
|
554
|
+
if (steps > 0 ? nextEnd <= end : nextStart >= start) return null;
|
|
555
|
+
|
|
556
|
+
return {kind: 'custom', start: nextStart.isoString, end: nextEnd.isoString};
|
|
557
|
+
}
|