@nepdatejs/datepicker 0.0.0-bootstrap → 0.1.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/LICENSE +21 -0
- package/README.md +109 -1
- package/dist/index.cjs +1325 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +420 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +1313 -0
- package/dist/index.js.map +1 -0
- package/dist/nepdate-datepicker.global.js +2 -0
- package/dist/nepdate-datepicker.global.js.map +1 -0
- package/package.json +63 -4
- package/src/base.ts +130 -0
- package/src/datetime.ts +397 -0
- package/src/index.ts +60 -0
- package/src/input.ts +128 -0
- package/src/mask.ts +42 -0
- package/src/multi.ts +318 -0
- package/src/picker.ts +322 -0
- package/src/presets.ts +146 -0
- package/src/range.ts +428 -0
- package/src/types.ts +250 -0
package/src/picker.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { maskDateText } from './mask';
|
|
2
|
+
import {
|
|
3
|
+
getConfig,
|
|
4
|
+
toCalendar,
|
|
5
|
+
toIsoDate,
|
|
6
|
+
type AdDate,
|
|
7
|
+
type CalendarDate,
|
|
8
|
+
type CalendarSystem,
|
|
9
|
+
} from '@nepdatejs/core';
|
|
10
|
+
import { createCalendar } from '@nepdatejs/calendar';
|
|
11
|
+
import {
|
|
12
|
+
createEmitter,
|
|
13
|
+
formatOf,
|
|
14
|
+
inputLimits,
|
|
15
|
+
isSingleDay,
|
|
16
|
+
localeOf,
|
|
17
|
+
panelOptions,
|
|
18
|
+
todayOf,
|
|
19
|
+
} from './base';
|
|
20
|
+
import {
|
|
21
|
+
describePreset,
|
|
22
|
+
resolveSinglePreset,
|
|
23
|
+
type PresetContext,
|
|
24
|
+
type PresetState,
|
|
25
|
+
} from './presets';
|
|
26
|
+
import {
|
|
27
|
+
checkDate,
|
|
28
|
+
detailOf,
|
|
29
|
+
equivalentText,
|
|
30
|
+
formatInput,
|
|
31
|
+
parseInput,
|
|
32
|
+
placeholderText,
|
|
33
|
+
readIso,
|
|
34
|
+
} from './input';
|
|
35
|
+
import type {
|
|
36
|
+
DateError,
|
|
37
|
+
DatePickerActions,
|
|
38
|
+
DatePickerOptions,
|
|
39
|
+
DatePickerState,
|
|
40
|
+
DatePickerStore,
|
|
41
|
+
} from './types';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A BS/AD date picker without UI: typed text ⇄ value, validation, the BS/AD
|
|
45
|
+
* switch and open/close, plus the panel's calendar store. The value is always
|
|
46
|
+
* an AD "YYYY-MM-DD" string (docs/plan D-003); all date work goes through core.
|
|
47
|
+
*
|
|
48
|
+
* Like the calendar, the picker keeps its own value; passing `value` through
|
|
49
|
+
* setOptions replaces it (controlled use). Passing back the value the user just
|
|
50
|
+
* typed leaves their text alone, so the cursor does not jump.
|
|
51
|
+
*/
|
|
52
|
+
export function createDatePicker(initial: DatePickerOptions = {}): DatePickerStore {
|
|
53
|
+
let options: DatePickerOptions = { ...initial };
|
|
54
|
+
let calendar: CalendarSystem = options.calendar ?? options.defaultCalendar ?? 'BS';
|
|
55
|
+
let date: AdDate | null = readIso(options.value ?? options.defaultValue);
|
|
56
|
+
let value = date ? toIsoDate(date) : '';
|
|
57
|
+
let text = formatInput(date, calendar, formatOf(options), localeOf(options));
|
|
58
|
+
let error: DateError | null = null;
|
|
59
|
+
let open = false;
|
|
60
|
+
let destroyed = false;
|
|
61
|
+
|
|
62
|
+
const panel = createCalendar({
|
|
63
|
+
...panelOptions(options, calendar),
|
|
64
|
+
selectionMode: 'single',
|
|
65
|
+
selected: date,
|
|
66
|
+
onSelect: (selection) => {
|
|
67
|
+
if (isSingleDay(selection)) {
|
|
68
|
+
pick(selection);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
let snapshot = build();
|
|
73
|
+
const emitter = createEmitter(() => {
|
|
74
|
+
snapshot = build();
|
|
75
|
+
});
|
|
76
|
+
panel.subscribe(() => {
|
|
77
|
+
emitter.notify();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function build(): DatePickerState {
|
|
81
|
+
const locale = localeOf(options);
|
|
82
|
+
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
value,
|
|
85
|
+
date: detailOf(date),
|
|
86
|
+
text,
|
|
87
|
+
calendar,
|
|
88
|
+
canSwitchCalendar: options.calendar === undefined,
|
|
89
|
+
open: open || options.inline === true,
|
|
90
|
+
inline: options.inline === true,
|
|
91
|
+
error,
|
|
92
|
+
equivalentText: error ? null : equivalentText(date, calendar, locale),
|
|
93
|
+
placeholder: placeholderText(calendar, locale),
|
|
94
|
+
presets: presetStates(),
|
|
95
|
+
calendarState: panel.getState(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function presetContext(): PresetContext {
|
|
100
|
+
return {
|
|
101
|
+
today: toCalendar(todayOf(options), 'AD'),
|
|
102
|
+
calendar,
|
|
103
|
+
weekStartsOn: options.weekStartsOn ?? getConfig().weekStartsOn,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Each shortcut's date, or null when it cannot be used now. */
|
|
108
|
+
function resolvedPresets(): { id: string; label: string; date: CalendarDate | null }[] {
|
|
109
|
+
const context = presetContext();
|
|
110
|
+
const limits = inputLimits(options, panel);
|
|
111
|
+
|
|
112
|
+
return (options.presets ?? []).map((preset) => {
|
|
113
|
+
const entry = describePreset(preset, options.locale, resolveSinglePreset);
|
|
114
|
+
const date = ((): CalendarDate | null => {
|
|
115
|
+
try {
|
|
116
|
+
const candidate = entry.resolve(context);
|
|
117
|
+
return checkDate(candidate, calendar, limits, localeOf(options))
|
|
118
|
+
? null
|
|
119
|
+
: candidate;
|
|
120
|
+
} catch {
|
|
121
|
+
return null; // e.g. outside the BS table
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
|
|
125
|
+
return { id: entry.id, label: entry.label, date };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function presetStates(): PresetState[] {
|
|
130
|
+
return resolvedPresets().map(({ id, label, date }) =>
|
|
131
|
+
Object.freeze({
|
|
132
|
+
id,
|
|
133
|
+
label,
|
|
134
|
+
isActive: date !== null && toIsoDate(date) === value,
|
|
135
|
+
isDisabled: date === null,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function syncPanel(showDate: boolean): void {
|
|
141
|
+
panel.setOptions({
|
|
142
|
+
...panelOptions(options, calendar),
|
|
143
|
+
selected: date,
|
|
144
|
+
...(showDate && date ? { visibleMonth: date } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Change the value; queue onChange when it differs and the user caused it. */
|
|
149
|
+
function setDate(next: AdDate | null, byUser: boolean): void {
|
|
150
|
+
const nextValue = next ? toIsoDate(next) : '';
|
|
151
|
+
const changed = nextValue !== value;
|
|
152
|
+
date = next;
|
|
153
|
+
value = nextValue;
|
|
154
|
+
syncPanel(next !== null);
|
|
155
|
+
if (changed && byUser) {
|
|
156
|
+
const detail = detailOf(next);
|
|
157
|
+
emitter.after(() => options.onChange?.(nextValue, detail));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function format(next: AdDate | null): string {
|
|
162
|
+
return formatInput(next, calendar, formatOf(options), localeOf(options));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function pick(selection: Parameters<DatePickerActions['select']>[0]): void {
|
|
166
|
+
emitter.act(() => {
|
|
167
|
+
const next = toCalendar(selection, 'AD');
|
|
168
|
+
error = null;
|
|
169
|
+
text = format(next);
|
|
170
|
+
setDate(next, true);
|
|
171
|
+
if (options.closeOnSelect ?? true) {
|
|
172
|
+
open = false;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const actions: DatePickerActions = {
|
|
178
|
+
type(next) {
|
|
179
|
+
if (destroyed) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
emitter.act(() => {
|
|
183
|
+
text = options.mask === false ? next : maskDateText(next, text);
|
|
184
|
+
const result = parseInput(
|
|
185
|
+
text,
|
|
186
|
+
calendar,
|
|
187
|
+
inputLimits(options, panel),
|
|
188
|
+
localeOf(options),
|
|
189
|
+
formatOf(options),
|
|
190
|
+
);
|
|
191
|
+
error = result.kind === 'error' ? result.error : null;
|
|
192
|
+
setDate(result.kind === 'date' ? result.date : null, true);
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
blur() {
|
|
196
|
+
emitter.act(() => {
|
|
197
|
+
if (!error) {
|
|
198
|
+
text = format(date);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
clear() {
|
|
203
|
+
emitter.act(() => {
|
|
204
|
+
text = '';
|
|
205
|
+
error = null;
|
|
206
|
+
setDate(null, true);
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
setValue(next) {
|
|
210
|
+
emitter.act(() => {
|
|
211
|
+
applyValue(next);
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
open() {
|
|
215
|
+
emitter.act(() => {
|
|
216
|
+
open = true;
|
|
217
|
+
panel.actions.goTo(date ?? todayOf(options));
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
close() {
|
|
221
|
+
emitter.act(() => {
|
|
222
|
+
open = false;
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
toggle() {
|
|
226
|
+
if (open) {
|
|
227
|
+
actions.close();
|
|
228
|
+
} else {
|
|
229
|
+
actions.open();
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
switchCalendar(next) {
|
|
233
|
+
if (options.calendar !== undefined || next === calendar || destroyed) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
emitter.act(() => {
|
|
237
|
+
calendar = next;
|
|
238
|
+
if (!error) {
|
|
239
|
+
text = format(date);
|
|
240
|
+
}
|
|
241
|
+
syncPanel(date !== null);
|
|
242
|
+
emitter.after(() => options.onCalendarChange?.(next));
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
select(day) {
|
|
246
|
+
let picked = false;
|
|
247
|
+
if (!destroyed) {
|
|
248
|
+
emitter.act(() => {
|
|
249
|
+
picked = panel.actions.select(day);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return picked;
|
|
254
|
+
},
|
|
255
|
+
selectToday() {
|
|
256
|
+
return actions.select(todayOf(options));
|
|
257
|
+
},
|
|
258
|
+
reset() {
|
|
259
|
+
emitter.act(() => {
|
|
260
|
+
const next = readIso(options.defaultValue);
|
|
261
|
+
error = null;
|
|
262
|
+
text = format(next);
|
|
263
|
+
setDate(next, true);
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
applyPreset(id) {
|
|
267
|
+
const preset = resolvedPresets().find((entry) => entry.id === id);
|
|
268
|
+
if (destroyed || !preset?.date) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
pick(preset.date);
|
|
272
|
+
|
|
273
|
+
return true;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** An outside value: replaces the text only when it is a different value (echo guard). */
|
|
278
|
+
function applyValue(next: string | undefined): void {
|
|
279
|
+
const nextDate = readIso(next);
|
|
280
|
+
if ((nextDate ? toIsoDate(nextDate) : '') === value) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
error = null;
|
|
284
|
+
text = format(nextDate);
|
|
285
|
+
setDate(nextDate, false);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
getState: () => snapshot,
|
|
290
|
+
subscribe: (listener) => emitter.subscribe(listener),
|
|
291
|
+
setOptions(changes) {
|
|
292
|
+
if (destroyed) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
emitter.act(() => {
|
|
296
|
+
const before = { calendar, locale: localeOf(options), format: formatOf(options) };
|
|
297
|
+
options = { ...options, ...changes };
|
|
298
|
+
if (options.calendar !== undefined) {
|
|
299
|
+
calendar = options.calendar;
|
|
300
|
+
}
|
|
301
|
+
if ('value' in changes) {
|
|
302
|
+
applyValue(changes.value);
|
|
303
|
+
}
|
|
304
|
+
const looksChanged =
|
|
305
|
+
before.calendar !== calendar ||
|
|
306
|
+
before.locale !== localeOf(options) ||
|
|
307
|
+
before.format !== formatOf(options);
|
|
308
|
+
if (looksChanged && !error) {
|
|
309
|
+
text = format(date);
|
|
310
|
+
}
|
|
311
|
+
syncPanel(false);
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
actions,
|
|
315
|
+
calendar: panel,
|
|
316
|
+
destroy() {
|
|
317
|
+
destroyed = true;
|
|
318
|
+
panel.destroy();
|
|
319
|
+
emitter.clear();
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDays,
|
|
3
|
+
addMonths,
|
|
4
|
+
endOfMonth,
|
|
5
|
+
endOfWeek,
|
|
6
|
+
endOfYear,
|
|
7
|
+
fiscalYear,
|
|
8
|
+
fiscalYearRange,
|
|
9
|
+
getLocale,
|
|
10
|
+
startOfMonth,
|
|
11
|
+
startOfWeek,
|
|
12
|
+
startOfYear,
|
|
13
|
+
toCalendar,
|
|
14
|
+
type AdDate,
|
|
15
|
+
type CalendarDate,
|
|
16
|
+
type CalendarSystem,
|
|
17
|
+
type LocaleCode,
|
|
18
|
+
type Weekday,
|
|
19
|
+
} from '@nepdatejs/core';
|
|
20
|
+
|
|
21
|
+
/** Built-in shortcuts for a single date. */
|
|
22
|
+
export type SinglePresetId = 'today' | 'yesterday' | 'tomorrow';
|
|
23
|
+
|
|
24
|
+
/** Built-in shortcuts for a range. Weeks, months and years count in the calendar on screen. */
|
|
25
|
+
export type RangePresetId =
|
|
26
|
+
| 'today'
|
|
27
|
+
| 'yesterday'
|
|
28
|
+
| 'thisWeek'
|
|
29
|
+
| 'lastWeek'
|
|
30
|
+
| 'last7Days'
|
|
31
|
+
| 'last30Days'
|
|
32
|
+
| 'thisMonth'
|
|
33
|
+
| 'lastMonth'
|
|
34
|
+
| 'thisYear'
|
|
35
|
+
| 'thisFiscalYear'
|
|
36
|
+
| 'lastFiscalYear';
|
|
37
|
+
|
|
38
|
+
/** What a preset gets to work out its date: today (in the configured zone), the calendar on screen, the week start. */
|
|
39
|
+
export interface PresetContext {
|
|
40
|
+
readonly today: AdDate;
|
|
41
|
+
readonly calendar: CalendarSystem;
|
|
42
|
+
readonly weekStartsOn: Weekday;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PresetRange {
|
|
46
|
+
readonly start: CalendarDate;
|
|
47
|
+
readonly end: CalendarDate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** An app-defined shortcut, e.g. { id: 'admission', label: 'Admission day', resolve: () => bsDate(2083, 1, 15) }. */
|
|
51
|
+
export interface CustomPreset<R> {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly label: string;
|
|
54
|
+
readonly resolve: (context: PresetContext) => R;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type SinglePreset = SinglePresetId | CustomPreset<CalendarDate>;
|
|
58
|
+
export type RangePreset = RangePresetId | CustomPreset<PresetRange>;
|
|
59
|
+
|
|
60
|
+
/** One shortcut as the panel shows it. */
|
|
61
|
+
export interface PresetState {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly label: string;
|
|
64
|
+
/** The current value is exactly this preset's date or range. */
|
|
65
|
+
readonly isActive: boolean;
|
|
66
|
+
/** Outside min/max, too short/long, or outside the BS table. */
|
|
67
|
+
readonly isDisabled: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The date a built-in single preset stands for. */
|
|
71
|
+
export function resolveSinglePreset(
|
|
72
|
+
id: SinglePresetId,
|
|
73
|
+
context: PresetContext,
|
|
74
|
+
): CalendarDate {
|
|
75
|
+
const today = toCalendar(context.today, context.calendar);
|
|
76
|
+
switch (id) {
|
|
77
|
+
case 'today':
|
|
78
|
+
return today;
|
|
79
|
+
case 'yesterday':
|
|
80
|
+
return addDays(today, -1);
|
|
81
|
+
case 'tomorrow':
|
|
82
|
+
return addDays(today, 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The range a built-in range preset stands for, in the calendar on screen. */
|
|
87
|
+
export function resolveRangePreset(
|
|
88
|
+
id: RangePresetId,
|
|
89
|
+
context: PresetContext,
|
|
90
|
+
): PresetRange {
|
|
91
|
+
const today = toCalendar(context.today, context.calendar);
|
|
92
|
+
const week = context.weekStartsOn;
|
|
93
|
+
switch (id) {
|
|
94
|
+
case 'today':
|
|
95
|
+
return { start: today, end: today };
|
|
96
|
+
case 'yesterday': {
|
|
97
|
+
const day = addDays(today, -1);
|
|
98
|
+
return { start: day, end: day };
|
|
99
|
+
}
|
|
100
|
+
case 'thisWeek':
|
|
101
|
+
return { start: startOfWeek(today, week), end: endOfWeek(today, week) };
|
|
102
|
+
case 'lastWeek': {
|
|
103
|
+
const day = addDays(today, -7);
|
|
104
|
+
return { start: startOfWeek(day, week), end: endOfWeek(day, week) };
|
|
105
|
+
}
|
|
106
|
+
case 'last7Days':
|
|
107
|
+
return { start: addDays(today, -6), end: today };
|
|
108
|
+
case 'last30Days':
|
|
109
|
+
return { start: addDays(today, -29), end: today };
|
|
110
|
+
case 'thisMonth':
|
|
111
|
+
return { start: startOfMonth(today), end: endOfMonth(today) };
|
|
112
|
+
case 'lastMonth': {
|
|
113
|
+
const month = addMonths(startOfMonth(today), -1);
|
|
114
|
+
return { start: month, end: endOfMonth(month) };
|
|
115
|
+
}
|
|
116
|
+
case 'thisYear':
|
|
117
|
+
return { start: startOfYear(today), end: endOfYear(today) };
|
|
118
|
+
case 'thisFiscalYear': {
|
|
119
|
+
const fy = fiscalYear(today);
|
|
120
|
+
return { start: fy.firstDay, end: fy.lastDay };
|
|
121
|
+
}
|
|
122
|
+
case 'lastFiscalYear': {
|
|
123
|
+
const fy = fiscalYearRange(fiscalYear(today).startYear - 1);
|
|
124
|
+
return { start: fy.firstDay, end: fy.lastDay };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Shared by both pickers: label + resolver for a preset entry. */
|
|
130
|
+
export function describePreset<R, Id extends string>(
|
|
131
|
+
preset: Id | CustomPreset<R>,
|
|
132
|
+
locale: LocaleCode | undefined,
|
|
133
|
+
resolveBuiltIn: (id: Id, context: PresetContext) => R,
|
|
134
|
+
): { id: string; label: string; resolve: (context: PresetContext) => R } {
|
|
135
|
+
if (typeof preset === 'string') {
|
|
136
|
+
const labels = getLocale(locale).presets as Record<string, string>;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
id: preset,
|
|
140
|
+
label: labels[preset] ?? preset,
|
|
141
|
+
resolve: (context) => resolveBuiltIn(preset, context),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return preset;
|
|
146
|
+
}
|