cx 24.11.4 → 25.1.1

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.
@@ -1,617 +1,618 @@
1
- import { StringTemplate } from "../../data/StringTemplate";
2
- import { Culture } from "../../ui/Culture";
3
- import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager";
4
- import "../../ui/Format";
5
- import { Localization } from "../../ui/Localization";
6
- import { VDOM, Widget } from "../../ui/Widget";
7
- import { KeyCode } from "../../util/KeyCode";
8
- import { dateDiff } from "../../util/date/dateDiff";
9
- import { lowerBoundCheck } from "../../util/date/lowerBoundCheck";
10
- import { monthStart } from "../../util/date/monthStart";
11
- import { sameDate } from "../../util/date/sameDate";
12
- import { upperBoundCheck } from "../../util/date/upperBoundCheck";
13
- import { zeroTime } from "../../util/date/zeroTime";
14
- import DropdownIcon from "../icons/drop-down";
15
- import ForwardIcon from "../icons/forward";
16
- import {
17
- tooltipMouseLeave,
18
- tooltipMouseMove,
19
- tooltipParentDidMount,
20
- tooltipParentWillReceiveProps,
21
- tooltipParentWillUnmount,
22
- } from "../overlay/tooltip-ops";
23
- import { Field, getFieldTooltip } from "./Field";
24
-
25
- export class Calendar extends Field {
26
- declareData() {
27
- super.declareData(
28
- {
29
- value: undefined,
30
- refDate: undefined,
31
- disabled: undefined,
32
- enabled: undefined,
33
- minValue: undefined,
34
- minExclusive: undefined,
35
- maxValue: undefined,
36
- maxExclusive: undefined,
37
- focusable: undefined,
38
- dayData: undefined,
39
- },
40
- ...arguments,
41
- );
42
- }
43
-
44
- init() {
45
- if (this.unfocusable) this.focusable = false;
46
-
47
- super.init();
48
- }
49
-
50
- prepareData(context, { data }) {
51
- data.stateMods = {
52
- disabled: data.disabled,
53
- };
54
-
55
- if (data.value) {
56
- let d = new Date(data.value);
57
- if (!isNaN(d.getTime())) {
58
- data.date = zeroTime(d);
59
- }
60
- }
61
-
62
- if (data.refDate) data.refDate = zeroTime(new Date(data.refDate));
63
-
64
- if (data.maxValue) data.maxValue = zeroTime(new Date(data.maxValue));
65
-
66
- if (data.minValue) data.minValue = zeroTime(new Date(data.minValue));
67
-
68
- super.prepareData(...arguments);
69
- }
70
-
71
- validate(context, instance) {
72
- super.validate(context, instance);
73
- let { data, widget } = instance;
74
- if (!data.error && data.date) {
75
- let d;
76
- if (data.maxValue) {
77
- d = dateDiff(data.date, data.maxValue);
78
- if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue);
79
- else if (d == 0 && data.maxExclusive)
80
- data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue);
81
- }
82
-
83
- if (data.minValue) {
84
- d = dateDiff(data.date, data.minValue);
85
- if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue);
86
- else if (d == 0 && data.minExclusive)
87
- data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue);
88
- }
89
-
90
- if (widget.disabledDaysOfWeek) {
91
- if (widget.disabledDaysOfWeek.includes(data.date.getDay())) data.error = this.disabledDaysOfWeekErrorText;
92
- }
93
-
94
- if (data.dayData) {
95
- let date = new Date(data.value);
96
- let info = data.dayData[date.toDateString()];
97
- if (info && info.disabled) data.error = this.disabledDaysOfWeekErrorText;
98
- }
99
- }
100
- }
101
-
102
- renderInput(context, instance, key) {
103
- return (
104
- <CalendarCmp key={key} instance={instance} handleSelect={(e, date) => this.handleSelect(e, instance, date)} />
105
- );
106
- }
107
-
108
- handleSelect(e, instance, date) {
109
- let { store, data, widget } = instance;
110
-
111
- e.stopPropagation();
112
-
113
- if (data.disabled) return;
114
-
115
- if (!validationCheck(date, data)) return;
116
-
117
- if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date) === false) return;
118
-
119
- if (widget.partial) {
120
- let mixed = new Date(data.value);
121
- if (data.value && !isNaN(mixed)) {
122
- mixed.setFullYear(date.getFullYear());
123
- mixed.setMonth(date.getMonth());
124
- mixed.setDate(date.getDate());
125
- date = mixed;
126
- }
127
- }
128
-
129
- let encode = widget.encoding || Culture.getDefaultDateEncoding();
130
- instance.set("value", encode(date));
131
-
132
- if (this.onSelect) instance.invoke("onSelect", e, instance, date);
133
- }
134
- }
135
-
136
- Calendar.prototype.baseClass = "calendar";
137
- Calendar.prototype.highlightToday = true;
138
- Calendar.prototype.maxValueErrorText = "Select a date not after {0:d}.";
139
- Calendar.prototype.maxExclusiveErrorText = "Select a date before {0:d}.";
140
- Calendar.prototype.minValueErrorText = "Select a date not before {0:d}.";
141
- Calendar.prototype.minExclusiveErrorText = "Select a date after {0:d}.";
142
- Calendar.prototype.disabledDaysOfWeekErrorText = "Selected day of week is not allowed.";
143
- Calendar.prototype.suppressErrorsUntilVisited = false;
144
- Calendar.prototype.showTodayButton = false;
145
- Calendar.prototype.todayButtonText = "Today";
146
- Calendar.prototype.startWithMonday = false;
147
- Calendar.prototype.focusable = true;
148
-
149
- Localization.registerPrototype("cx/widgets/Calendar", Calendar);
150
-
151
- const validationCheck = (date, data, disabledDaysOfWeek) => {
152
- if (data.maxValue && !upperBoundCheck(date, data.maxValue, data.maxExclusive)) return false;
153
-
154
- if (data.minValue && !lowerBoundCheck(date, data.minValue, data.minExclusive)) return false;
155
-
156
- if (disabledDaysOfWeek && disabledDaysOfWeek.includes(date.getDay())) return false;
157
-
158
- if (data.dayData) {
159
- let day = data.dayData[date.toDateString()];
160
- if (day && (day.disabled || day.unselectable)) return false;
161
- }
162
-
163
- return true;
164
- };
165
-
166
- export class CalendarCmp extends VDOM.Component {
167
- constructor(props) {
168
- super(props);
169
- let { data } = props.instance;
170
-
171
- let refDate = data.refDate ? data.refDate : data.date || zeroTime(new Date());
172
-
173
- this.state = Object.assign(
174
- {
175
- hover: false,
176
- focus: false,
177
- cursor: zeroTime(data.date || refDate),
178
- activeView: "calendar",
179
- },
180
- this.getPage(refDate),
181
- );
182
-
183
- this.handleMouseMove = this.handleMouseMove.bind(this);
184
- this.handleMouseDown = this.handleMouseDown.bind(this);
185
- }
186
-
187
- getPage(refDate) {
188
- refDate = monthStart(refDate); //make a copy
189
-
190
- let startWithMonday = this.props.instance.widget.startWithMonday;
191
-
192
- let startDay = startWithMonday ? 1 : 0;
193
- let startDate = new Date(refDate);
194
- while (startDate.getDay() != startDay) startDate.setDate(startDate.getDate() - 1);
195
-
196
- let endDate = new Date(refDate);
197
- endDate.setMonth(refDate.getMonth() + 1);
198
- endDate.setDate(endDate.getDate() - 1);
199
-
200
- let endDay = startWithMonday ? 0 : 6;
201
- while (endDate.getDay() != endDay) endDate.setDate(endDate.getDate() + 1);
202
-
203
- return {
204
- refDate,
205
- startDate,
206
- endDate,
207
- };
208
- }
209
-
210
- moveCursor(e, date, options = {}) {
211
- e.preventDefault();
212
- e.stopPropagation();
213
-
214
- date = zeroTime(date);
215
- if (date.getTime() == this.state.cursor.getTime()) return;
216
-
217
- let refDate = this.state.refDate;
218
-
219
- if (options.movePage || date < this.state.startDate || date > this.state.endDate) refDate = date;
220
-
221
- this.setState({
222
- ...this.getPage(refDate),
223
- cursor: date,
224
- });
225
- }
226
-
227
- move(e, period, delta) {
228
- e.preventDefault();
229
- e.stopPropagation();
230
-
231
- let refDate = this.state.refDate;
232
-
233
- switch (period) {
234
- case "y":
235
- refDate.setFullYear(refDate.getFullYear() + delta);
236
- break;
237
-
238
- case "m":
239
- refDate.setMonth(refDate.getMonth() + delta);
240
- break;
241
- }
242
-
243
- let page = this.getPage(refDate);
244
- if (this.state.cursor < page.startDate) page.cursor = page.startDate;
245
- else if (this.state.cursor > page.endDate) page.cursor = page.endDate;
246
-
247
- this.setState(page);
248
- }
249
-
250
- handleKeyPress(e) {
251
- let cursor = new Date(this.state.cursor);
252
-
253
- switch (e.keyCode) {
254
- case KeyCode.enter:
255
- this.props.handleSelect(e, this.state.cursor);
256
- break;
257
-
258
- case KeyCode.left:
259
- cursor.setDate(cursor.getDate() - 1);
260
- this.moveCursor(e, cursor);
261
- break;
262
-
263
- case KeyCode.right:
264
- cursor.setDate(cursor.getDate() + 1);
265
- this.moveCursor(e, cursor);
266
- break;
267
-
268
- case KeyCode.up:
269
- cursor.setDate(cursor.getDate() - 7);
270
- this.moveCursor(e, cursor);
271
- break;
272
-
273
- case KeyCode.down:
274
- cursor.setDate(cursor.getDate() + 7);
275
- this.moveCursor(e, cursor);
276
- break;
277
-
278
- case KeyCode.pageUp:
279
- cursor.setMonth(cursor.getMonth() - 1);
280
- this.moveCursor(e, cursor, { movePage: true });
281
- break;
282
-
283
- case KeyCode.pageDown:
284
- cursor.setMonth(cursor.getMonth() + 1);
285
- this.moveCursor(e, cursor, { movePage: true });
286
- break;
287
-
288
- case KeyCode.home:
289
- cursor.setDate(1);
290
- this.moveCursor(e, cursor, { movePage: true });
291
- break;
292
-
293
- case KeyCode.end:
294
- cursor.setMonth(cursor.getMonth() + 1);
295
- cursor.setDate(0);
296
- this.moveCursor(e, cursor, { movePage: true });
297
- break;
298
-
299
- default:
300
- let { instance } = this.props;
301
- let { widget } = instance;
302
- if (widget.onKeyDown) instance.invoke("onKeyDown", e, instance);
303
- break;
304
- }
305
- }
306
-
307
- handleWheel(e) {
308
- e.preventDefault();
309
- e.stopPropagation();
310
-
311
- let cursor = new Date(this.state.cursor);
312
-
313
- if (e.deltaY < 0) {
314
- cursor.setMonth(cursor.getMonth() - 1);
315
- this.moveCursor(e, cursor, { movePage: true });
316
- } else if (e.deltaY > 0) {
317
- cursor.setMonth(cursor.getMonth() + 1);
318
- this.moveCursor(e, cursor, { movePage: true });
319
- }
320
- }
321
-
322
- handleBlur(e) {
323
- FocusManager.nudge();
324
- let { instance } = this.props;
325
- let { widget } = instance;
326
- if (widget.onBlur) instance.invoke("onBlur", e, instance);
327
- this.setState({
328
- focus: false,
329
- });
330
- }
331
-
332
- handleFocus(e) {
333
- oneFocusOut(this, this.el, this.handleFocusOut.bind(this));
334
- this.setState({
335
- focus: true,
336
- });
337
- }
338
-
339
- handleFocusOut() {
340
- let { instance } = this.props;
341
- let { widget } = instance;
342
- if (widget.onFocusOut) instance.invoke("onFocusOut", null, instance);
343
- }
344
-
345
- handleMouseLeave(e) {
346
- tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance));
347
- this.setState({
348
- hover: false,
349
- });
350
- }
351
-
352
- handleMouseEnter(e) {
353
- this.setState({
354
- hover: true,
355
- });
356
- }
357
-
358
- handleMouseMove(e) {
359
- this.moveCursor(e, readDate(e.target.dataset));
360
- }
361
-
362
- handleMouseDown(e) {
363
- this.props.handleSelect(e, readDate(e.target.dataset));
364
- }
365
-
366
- componentDidMount() {
367
- //calendar doesn't bring up keyboard so it's ok to focus it even on mobile
368
- if (this.props.instance.widget.autoFocus) this.el.focus();
369
-
370
- tooltipParentDidMount(this.el, ...getFieldTooltip(this.props.instance));
371
- this.el.addEventListener("wheel", (e) => this.handleWheel(e));
372
- }
373
-
374
- UNSAFE_componentWillReceiveProps(props) {
375
- let { data } = props.instance;
376
- if (data.date)
377
- this.setState({
378
- ...this.getPage(data.date),
379
- value: data.date,
380
- });
381
-
382
- tooltipParentWillReceiveProps(this.el, ...getFieldTooltip(props.instance));
383
- }
384
-
385
- componentWillUnmount() {
386
- offFocusOut(this);
387
- tooltipParentWillUnmount(this.props.instance);
388
- }
389
-
390
- showYearDropdown() {
391
- this.setState({
392
- activeView: "year-picker",
393
- yearPickerHeight: this.el.firstChild.offsetHeight,
394
- });
395
- }
396
-
397
- handleYearSelect(e, year) {
398
- e.preventDefault();
399
- e.stopPropagation();
400
- let refDate = new Date(this.state.refDate);
401
- refDate.setFullYear(year);
402
- this.setState({
403
- ...this.getPage(refDate),
404
- refDate,
405
- activeView: "calendar",
406
- });
407
- }
408
-
409
- renderYearPicker() {
410
- let { data, widget } = this.props.instance;
411
- let minYear = data.minValue?.getFullYear();
412
- let maxYear = data.maxValue?.getFullYear();
413
- let { CSS } = this.props.instance.widget;
414
-
415
- let years = [];
416
- let currentYear = new Date().getFullYear();
417
- let midYear = currentYear - (currentYear % 5);
418
- let refYear = new Date(this.state.refDate).getFullYear();
419
- for (let i = midYear - 100; i <= midYear + 100; i++) {
420
- years.push(i);
421
- }
422
-
423
- let rows = [];
424
- for (let i = 0; i < years.length; i += 5) {
425
- rows.push(years.slice(i, i + 5));
426
- }
427
- return (
428
- <div
429
- className={CSS.element(widget.baseClass, "year-picker")}
430
- style={{
431
- height: this.state.yearPickerHeight,
432
- }}
433
- ref={(el) => {
434
- if (el) {
435
- el.addEventListener("wheel", (e) => {
436
- e.stopPropagation();
437
- });
438
-
439
- let activeYear = el.querySelector("." + CSS.state("selected"));
440
- if (activeYear) activeYear.scrollIntoView({ block: "center", behavior: "instant" });
441
- }
442
- }}
443
- >
444
- <table>
445
- <tbody>
446
- {rows.map((row, rowIndex) => (
447
- <tr key={rowIndex}>
448
- {row.map((year) => (
449
- <td
450
- key={year}
451
- className={CSS.element(this.props.instance.widget.baseClass, "year-option", {
452
- unselectable: (minYear && year < minYear) || (maxYear && year > maxYear),
453
- selected: year === refYear,
454
- active: year === currentYear,
455
- })}
456
- onClick={(e) => this.handleYearSelect(e, year)}
457
- >
458
- {year}
459
- </td>
460
- ))}
461
- </tr>
462
- ))}
463
- </tbody>
464
- </table>
465
- </div>
466
- );
467
- }
468
-
469
- render() {
470
- let { data, widget } = this.props.instance;
471
- let { CSS, baseClass, disabledDaysOfWeek, startWithMonday } = widget;
472
-
473
- let { refDate, startDate, endDate } = this.getPage(this.state.refDate);
474
-
475
- let month = refDate.getMonth();
476
- let year = refDate.getFullYear();
477
- let weeks = [];
478
- let date = startDate;
479
-
480
- let empty = {};
481
-
482
- let today = zeroTime(new Date());
483
- while (date >= startDate && date <= endDate) {
484
- let days = [];
485
- for (let i = 0; i < 7; i++) {
486
- let dayInfo = (data.dayData && data.dayData[date.toDateString()]) || empty;
487
- let unselectable = !validationCheck(date, data, disabledDaysOfWeek);
488
- let classNames = CSS.expand(
489
- CSS.element(baseClass, "day", {
490
- outside: month != date.getMonth(),
491
- unselectable: unselectable,
492
- selected: data.date && sameDate(data.date, date),
493
- cursor:
494
- (this.state.hover || this.state.focus) && this.state.cursor && sameDate(this.state.cursor, date),
495
- today: widget.highlightToday && sameDate(date, today),
496
- }),
497
- dayInfo.className,
498
- CSS.mod(dayInfo.mod),
499
- );
500
- let dateInst = new Date(date);
501
- days.push(
502
- <td
503
- key={i}
504
- className={classNames}
505
- style={CSS.parseStyle(dayInfo.style)}
506
- data-year={dateInst.getFullYear()}
507
- data-month={dateInst.getMonth() + 1}
508
- data-date={dateInst.getDate()}
509
- onMouseMove={unselectable ? null : this.handleMouseMove}
510
- onMouseDown={unselectable ? null : this.handleMouseDown}
511
- >
512
- {date.getDate()}
513
- </td>,
514
- );
515
- date.setDate(date.getDate() + 1);
516
- }
517
- weeks.push(
518
- <tr key={weeks.length} className={CSS.element(baseClass, "week")}>
519
- <td />
520
- {days}
521
- <td />
522
- </tr>,
523
- );
524
- }
525
-
526
- let culture = Culture.getDateTimeCulture();
527
- let monthNames = culture.getMonthNames("long");
528
- let dayNames = culture.getWeekdayNames("short").map((x) => x.substr(0, 2));
529
- if (startWithMonday) dayNames = [...dayNames.slice(1), dayNames[0]];
530
-
531
- return (
532
- <div
533
- className={data.classNames}
534
- tabIndex={data.disabled || !data.focusable ? null : data.tabIndex || 0}
535
- onKeyDown={(e) => this.handleKeyPress(e)}
536
- onMouseDown={(e) => {
537
- // prevent losing focus from the input field
538
- if (!data.focusable) {
539
- e.preventDefault();
540
- }
541
- e.stopPropagation();
542
- }}
543
- ref={(el) => {
544
- this.el = el;
545
- }}
546
- onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))}
547
- onMouseLeave={(e) => this.handleMouseLeave(e)}
548
- onMouseEnter={(e) => this.handleMouseEnter(e)}
549
- // onWheel={(e) => this.handleWheel(e)}
550
- onFocus={(e) => this.handleFocus(e)}
551
- onBlur={(e) => this.handleBlur(e)}
552
- >
553
- {this.state.activeView == "calendar" && (
554
- <table>
555
- <thead>
556
- <tr key="h" className={CSS.element(baseClass, "header")}>
557
- <td />
558
- <td onClick={(e) => this.move(e, "y", -1)}>
559
- <ForwardIcon className={CSS.element(baseClass, "icon-prev-year")} />
560
- </td>
561
- <td onClick={(e) => this.move(e, "m", -1)}>
562
- <DropdownIcon className={CSS.element(baseClass, "icon-prev-month")} />
563
- </td>
564
- <th className={CSS.element(baseClass, "display")} colSpan="3">
565
- {monthNames[month]}
566
- <br />
567
- <span
568
- onClick={() => this.showYearDropdown()}
569
- className={CSS.element(baseClass, "year-name")}
570
- >
571
- {year}
572
- </span>
573
- </th>
574
- <td onClick={(e) => this.move(e, "m", +1)}>
575
- <DropdownIcon className={CSS.element(baseClass, "icon-next-month")} />
576
- </td>
577
- <td onClick={(e) => this.move(e, "y", +1)}>
578
- <ForwardIcon className={CSS.element(baseClass, "icon-next-year")} />
579
- </td>
580
- <td />
581
- </tr>
582
- <tr key="d" className={CSS.element(baseClass, "day-names")}>
583
- <td />
584
- {dayNames.map((name, i) => (
585
- <th key={i}>{name}</th>
586
- ))}
587
- <td />
588
- </tr>
589
- </thead>
590
- <tbody>{weeks}</tbody>
591
- </table>
592
- )}
593
- {this.state.activeView == "calendar" && widget.showTodayButton && (
594
- <div className={CSS.element(baseClass, "toolbar")}>
595
- <button
596
- className={CSS.expand(CSS.element(baseClass, "today-button"), CSS.block("button", "hollow"))}
597
- data-year={today.getFullYear()}
598
- data-month={today.getMonth() + 1}
599
- data-date={today.getDate()}
600
- onClick={(e) => {
601
- this.handleMouseDown(e);
602
- }}
603
- >
604
- {widget.todayButtonText}
605
- </button>
606
- </div>
607
- )}
608
-
609
- {this.state.activeView == "year-picker" && this.renderYearPicker()}
610
- </div>
611
- );
612
- }
613
- }
614
-
615
- const readDate = (ds) => new Date(Number(ds.year), Number(ds.month) - 1, Number(ds.date));
616
-
617
- Widget.alias("calendar", Calendar);
1
+ import { StringTemplate } from "../../data/StringTemplate";
2
+ import { Culture } from "../../ui/Culture";
3
+ import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager";
4
+ import "../../ui/Format";
5
+ import { Localization } from "../../ui/Localization";
6
+ import { VDOM, Widget } from "../../ui/Widget";
7
+ import { parseDateInvariant } from "../../util";
8
+ import { KeyCode } from "../../util/KeyCode";
9
+ import { dateDiff } from "../../util/date/dateDiff";
10
+ import { lowerBoundCheck } from "../../util/date/lowerBoundCheck";
11
+ import { monthStart } from "../../util/date/monthStart";
12
+ import { sameDate } from "../../util/date/sameDate";
13
+ import { upperBoundCheck } from "../../util/date/upperBoundCheck";
14
+ import { zeroTime } from "../../util/date/zeroTime";
15
+ import DropdownIcon from "../icons/drop-down";
16
+ import ForwardIcon from "../icons/forward";
17
+ import {
18
+ tooltipMouseLeave,
19
+ tooltipMouseMove,
20
+ tooltipParentDidMount,
21
+ tooltipParentWillReceiveProps,
22
+ tooltipParentWillUnmount,
23
+ } from "../overlay/tooltip-ops";
24
+ import { Field, getFieldTooltip } from "./Field";
25
+
26
+ export class Calendar extends Field {
27
+ declareData() {
28
+ super.declareData(
29
+ {
30
+ value: undefined,
31
+ refDate: undefined,
32
+ disabled: undefined,
33
+ enabled: undefined,
34
+ minValue: undefined,
35
+ minExclusive: undefined,
36
+ maxValue: undefined,
37
+ maxExclusive: undefined,
38
+ focusable: undefined,
39
+ dayData: undefined,
40
+ },
41
+ ...arguments,
42
+ );
43
+ }
44
+
45
+ init() {
46
+ if (this.unfocusable) this.focusable = false;
47
+
48
+ super.init();
49
+ }
50
+
51
+ prepareData(context, { data }) {
52
+ data.stateMods = {
53
+ disabled: data.disabled,
54
+ };
55
+
56
+ if (data.value) {
57
+ let d = parseDateInvariant(data.value);
58
+ if (!isNaN(d.getTime())) {
59
+ data.date = zeroTime(d);
60
+ }
61
+ }
62
+
63
+ if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate));
64
+
65
+ if (data.maxValue) data.maxValue = zeroTime(parseDateInvariant(data.maxValue));
66
+
67
+ if (data.minValue) data.minValue = zeroTime(parseDateInvariant(data.minValue));
68
+
69
+ super.prepareData(...arguments);
70
+ }
71
+
72
+ validate(context, instance) {
73
+ super.validate(context, instance);
74
+ let { data, widget } = instance;
75
+ if (!data.error && data.date) {
76
+ let d;
77
+ if (data.maxValue) {
78
+ d = dateDiff(data.date, data.maxValue);
79
+ if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue);
80
+ else if (d == 0 && data.maxExclusive)
81
+ data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue);
82
+ }
83
+
84
+ if (data.minValue) {
85
+ d = dateDiff(data.date, data.minValue);
86
+ if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue);
87
+ else if (d == 0 && data.minExclusive)
88
+ data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue);
89
+ }
90
+
91
+ if (widget.disabledDaysOfWeek) {
92
+ if (widget.disabledDaysOfWeek.includes(data.date.getDay())) data.error = this.disabledDaysOfWeekErrorText;
93
+ }
94
+
95
+ if (data.dayData) {
96
+ let date = parseDateInvariant(data.value);
97
+ let info = data.dayData[date.toDateString()];
98
+ if (info && info.disabled) data.error = this.disabledDaysOfWeekErrorText;
99
+ }
100
+ }
101
+ }
102
+
103
+ renderInput(context, instance, key) {
104
+ return (
105
+ <CalendarCmp key={key} instance={instance} handleSelect={(e, date) => this.handleSelect(e, instance, date)} />
106
+ );
107
+ }
108
+
109
+ handleSelect(e, instance, date) {
110
+ let { store, data, widget } = instance;
111
+
112
+ e.stopPropagation();
113
+
114
+ if (data.disabled) return;
115
+
116
+ if (!validationCheck(date, data)) return;
117
+
118
+ if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date) === false) return;
119
+
120
+ if (widget.partial) {
121
+ let mixed = parseDateInvariant(data.value);
122
+ if (data.value && !isNaN(mixed)) {
123
+ mixed.setFullYear(date.getFullYear());
124
+ mixed.setMonth(date.getMonth());
125
+ mixed.setDate(date.getDate());
126
+ date = mixed;
127
+ }
128
+ }
129
+
130
+ let encode = widget.encoding || Culture.getDefaultDateEncoding();
131
+ instance.set("value", encode(date));
132
+
133
+ if (this.onSelect) instance.invoke("onSelect", e, instance, date);
134
+ }
135
+ }
136
+
137
+ Calendar.prototype.baseClass = "calendar";
138
+ Calendar.prototype.highlightToday = true;
139
+ Calendar.prototype.maxValueErrorText = "Select a date not after {0:d}.";
140
+ Calendar.prototype.maxExclusiveErrorText = "Select a date before {0:d}.";
141
+ Calendar.prototype.minValueErrorText = "Select a date not before {0:d}.";
142
+ Calendar.prototype.minExclusiveErrorText = "Select a date after {0:d}.";
143
+ Calendar.prototype.disabledDaysOfWeekErrorText = "Selected day of week is not allowed.";
144
+ Calendar.prototype.suppressErrorsUntilVisited = false;
145
+ Calendar.prototype.showTodayButton = false;
146
+ Calendar.prototype.todayButtonText = "Today";
147
+ Calendar.prototype.startWithMonday = false;
148
+ Calendar.prototype.focusable = true;
149
+
150
+ Localization.registerPrototype("cx/widgets/Calendar", Calendar);
151
+
152
+ const validationCheck = (date, data, disabledDaysOfWeek) => {
153
+ if (data.maxValue && !upperBoundCheck(date, data.maxValue, data.maxExclusive)) return false;
154
+
155
+ if (data.minValue && !lowerBoundCheck(date, data.minValue, data.minExclusive)) return false;
156
+
157
+ if (disabledDaysOfWeek && disabledDaysOfWeek.includes(date.getDay())) return false;
158
+
159
+ if (data.dayData) {
160
+ let day = data.dayData[date.toDateString()];
161
+ if (day && (day.disabled || day.unselectable)) return false;
162
+ }
163
+
164
+ return true;
165
+ };
166
+
167
+ export class CalendarCmp extends VDOM.Component {
168
+ constructor(props) {
169
+ super(props);
170
+ let { data } = props.instance;
171
+
172
+ let refDate = data.refDate ? data.refDate : data.date || zeroTime(new Date());
173
+
174
+ this.state = Object.assign(
175
+ {
176
+ hover: false,
177
+ focus: false,
178
+ cursor: zeroTime(data.date || refDate),
179
+ activeView: "calendar",
180
+ },
181
+ this.getPage(refDate),
182
+ );
183
+
184
+ this.handleMouseMove = this.handleMouseMove.bind(this);
185
+ this.handleMouseDown = this.handleMouseDown.bind(this);
186
+ }
187
+
188
+ getPage(refDate) {
189
+ refDate = monthStart(refDate); //make a copy
190
+
191
+ let startWithMonday = this.props.instance.widget.startWithMonday;
192
+
193
+ let startDay = startWithMonday ? 1 : 0;
194
+ let startDate = new Date(refDate);
195
+ while (startDate.getDay() != startDay) startDate.setDate(startDate.getDate() - 1);
196
+
197
+ let endDate = new Date(refDate);
198
+ endDate.setMonth(refDate.getMonth() + 1);
199
+ endDate.setDate(endDate.getDate() - 1);
200
+
201
+ let endDay = startWithMonday ? 0 : 6;
202
+ while (endDate.getDay() != endDay) endDate.setDate(endDate.getDate() + 1);
203
+
204
+ return {
205
+ refDate,
206
+ startDate,
207
+ endDate,
208
+ };
209
+ }
210
+
211
+ moveCursor(e, date, options = {}) {
212
+ e.preventDefault();
213
+ e.stopPropagation();
214
+
215
+ date = zeroTime(date);
216
+ if (date.getTime() == this.state.cursor.getTime()) return;
217
+
218
+ let refDate = this.state.refDate;
219
+
220
+ if (options.movePage || date < this.state.startDate || date > this.state.endDate) refDate = date;
221
+
222
+ this.setState({
223
+ ...this.getPage(refDate),
224
+ cursor: date,
225
+ });
226
+ }
227
+
228
+ move(e, period, delta) {
229
+ e.preventDefault();
230
+ e.stopPropagation();
231
+
232
+ let refDate = this.state.refDate;
233
+
234
+ switch (period) {
235
+ case "y":
236
+ refDate.setFullYear(refDate.getFullYear() + delta);
237
+ break;
238
+
239
+ case "m":
240
+ refDate.setMonth(refDate.getMonth() + delta);
241
+ break;
242
+ }
243
+
244
+ let page = this.getPage(refDate);
245
+ if (this.state.cursor < page.startDate) page.cursor = page.startDate;
246
+ else if (this.state.cursor > page.endDate) page.cursor = page.endDate;
247
+
248
+ this.setState(page);
249
+ }
250
+
251
+ handleKeyPress(e) {
252
+ let cursor = new Date(this.state.cursor);
253
+
254
+ switch (e.keyCode) {
255
+ case KeyCode.enter:
256
+ this.props.handleSelect(e, this.state.cursor);
257
+ break;
258
+
259
+ case KeyCode.left:
260
+ cursor.setDate(cursor.getDate() - 1);
261
+ this.moveCursor(e, cursor);
262
+ break;
263
+
264
+ case KeyCode.right:
265
+ cursor.setDate(cursor.getDate() + 1);
266
+ this.moveCursor(e, cursor);
267
+ break;
268
+
269
+ case KeyCode.up:
270
+ cursor.setDate(cursor.getDate() - 7);
271
+ this.moveCursor(e, cursor);
272
+ break;
273
+
274
+ case KeyCode.down:
275
+ cursor.setDate(cursor.getDate() + 7);
276
+ this.moveCursor(e, cursor);
277
+ break;
278
+
279
+ case KeyCode.pageUp:
280
+ cursor.setMonth(cursor.getMonth() - 1);
281
+ this.moveCursor(e, cursor, { movePage: true });
282
+ break;
283
+
284
+ case KeyCode.pageDown:
285
+ cursor.setMonth(cursor.getMonth() + 1);
286
+ this.moveCursor(e, cursor, { movePage: true });
287
+ break;
288
+
289
+ case KeyCode.home:
290
+ cursor.setDate(1);
291
+ this.moveCursor(e, cursor, { movePage: true });
292
+ break;
293
+
294
+ case KeyCode.end:
295
+ cursor.setMonth(cursor.getMonth() + 1);
296
+ cursor.setDate(0);
297
+ this.moveCursor(e, cursor, { movePage: true });
298
+ break;
299
+
300
+ default:
301
+ let { instance } = this.props;
302
+ let { widget } = instance;
303
+ if (widget.onKeyDown) instance.invoke("onKeyDown", e, instance);
304
+ break;
305
+ }
306
+ }
307
+
308
+ handleWheel(e) {
309
+ e.preventDefault();
310
+ e.stopPropagation();
311
+
312
+ let cursor = new Date(this.state.cursor);
313
+
314
+ if (e.deltaY < 0) {
315
+ cursor.setMonth(cursor.getMonth() - 1);
316
+ this.moveCursor(e, cursor, { movePage: true });
317
+ } else if (e.deltaY > 0) {
318
+ cursor.setMonth(cursor.getMonth() + 1);
319
+ this.moveCursor(e, cursor, { movePage: true });
320
+ }
321
+ }
322
+
323
+ handleBlur(e) {
324
+ FocusManager.nudge();
325
+ let { instance } = this.props;
326
+ let { widget } = instance;
327
+ if (widget.onBlur) instance.invoke("onBlur", e, instance);
328
+ this.setState({
329
+ focus: false,
330
+ });
331
+ }
332
+
333
+ handleFocus(e) {
334
+ oneFocusOut(this, this.el, this.handleFocusOut.bind(this));
335
+ this.setState({
336
+ focus: true,
337
+ });
338
+ }
339
+
340
+ handleFocusOut() {
341
+ let { instance } = this.props;
342
+ let { widget } = instance;
343
+ if (widget.onFocusOut) instance.invoke("onFocusOut", null, instance);
344
+ }
345
+
346
+ handleMouseLeave(e) {
347
+ tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance));
348
+ this.setState({
349
+ hover: false,
350
+ });
351
+ }
352
+
353
+ handleMouseEnter(e) {
354
+ this.setState({
355
+ hover: true,
356
+ });
357
+ }
358
+
359
+ handleMouseMove(e) {
360
+ this.moveCursor(e, readDate(e.target.dataset));
361
+ }
362
+
363
+ handleMouseDown(e) {
364
+ this.props.handleSelect(e, readDate(e.target.dataset));
365
+ }
366
+
367
+ componentDidMount() {
368
+ //calendar doesn't bring up keyboard so it's ok to focus it even on mobile
369
+ if (this.props.instance.widget.autoFocus) this.el.focus();
370
+
371
+ tooltipParentDidMount(this.el, ...getFieldTooltip(this.props.instance));
372
+ this.el.addEventListener("wheel", (e) => this.handleWheel(e));
373
+ }
374
+
375
+ UNSAFE_componentWillReceiveProps(props) {
376
+ let { data } = props.instance;
377
+ if (data.date)
378
+ this.setState({
379
+ ...this.getPage(data.date),
380
+ value: data.date,
381
+ });
382
+
383
+ tooltipParentWillReceiveProps(this.el, ...getFieldTooltip(props.instance));
384
+ }
385
+
386
+ componentWillUnmount() {
387
+ offFocusOut(this);
388
+ tooltipParentWillUnmount(this.props.instance);
389
+ }
390
+
391
+ showYearDropdown() {
392
+ this.setState({
393
+ activeView: "year-picker",
394
+ yearPickerHeight: this.el.firstChild.offsetHeight,
395
+ });
396
+ }
397
+
398
+ handleYearSelect(e, year) {
399
+ e.preventDefault();
400
+ e.stopPropagation();
401
+ let refDate = new Date(this.state.refDate);
402
+ refDate.setFullYear(year);
403
+ this.setState({
404
+ ...this.getPage(refDate),
405
+ refDate,
406
+ activeView: "calendar",
407
+ });
408
+ }
409
+
410
+ renderYearPicker() {
411
+ let { data, widget } = this.props.instance;
412
+ let minYear = data.minValue?.getFullYear();
413
+ let maxYear = data.maxValue?.getFullYear();
414
+ let { CSS } = this.props.instance.widget;
415
+
416
+ let years = [];
417
+ let currentYear = new Date().getFullYear();
418
+ let midYear = currentYear - (currentYear % 5);
419
+ let refYear = new Date(this.state.refDate).getFullYear();
420
+ for (let i = midYear - 100; i <= midYear + 100; i++) {
421
+ years.push(i);
422
+ }
423
+
424
+ let rows = [];
425
+ for (let i = 0; i < years.length; i += 5) {
426
+ rows.push(years.slice(i, i + 5));
427
+ }
428
+ return (
429
+ <div
430
+ className={CSS.element(widget.baseClass, "year-picker")}
431
+ style={{
432
+ height: this.state.yearPickerHeight,
433
+ }}
434
+ ref={(el) => {
435
+ if (el) {
436
+ el.addEventListener("wheel", (e) => {
437
+ e.stopPropagation();
438
+ });
439
+
440
+ let activeYear = el.querySelector("." + CSS.state("selected"));
441
+ if (activeYear) activeYear.scrollIntoView({ block: "center", behavior: "instant" });
442
+ }
443
+ }}
444
+ >
445
+ <table>
446
+ <tbody>
447
+ {rows.map((row, rowIndex) => (
448
+ <tr key={rowIndex}>
449
+ {row.map((year) => (
450
+ <td
451
+ key={year}
452
+ className={CSS.element(this.props.instance.widget.baseClass, "year-option", {
453
+ unselectable: (minYear && year < minYear) || (maxYear && year > maxYear),
454
+ selected: year === refYear,
455
+ active: year === currentYear,
456
+ })}
457
+ onClick={(e) => this.handleYearSelect(e, year)}
458
+ >
459
+ {year}
460
+ </td>
461
+ ))}
462
+ </tr>
463
+ ))}
464
+ </tbody>
465
+ </table>
466
+ </div>
467
+ );
468
+ }
469
+
470
+ render() {
471
+ let { data, widget } = this.props.instance;
472
+ let { CSS, baseClass, disabledDaysOfWeek, startWithMonday } = widget;
473
+
474
+ let { refDate, startDate, endDate } = this.getPage(this.state.refDate);
475
+
476
+ let month = refDate.getMonth();
477
+ let year = refDate.getFullYear();
478
+ let weeks = [];
479
+ let date = startDate;
480
+
481
+ let empty = {};
482
+
483
+ let today = zeroTime(new Date());
484
+ while (date >= startDate && date <= endDate) {
485
+ let days = [];
486
+ for (let i = 0; i < 7; i++) {
487
+ let dayInfo = (data.dayData && data.dayData[date.toDateString()]) || empty;
488
+ let unselectable = !validationCheck(date, data, disabledDaysOfWeek);
489
+ let classNames = CSS.expand(
490
+ CSS.element(baseClass, "day", {
491
+ outside: month != date.getMonth(),
492
+ unselectable: unselectable,
493
+ selected: data.date && sameDate(data.date, date),
494
+ cursor:
495
+ (this.state.hover || this.state.focus) && this.state.cursor && sameDate(this.state.cursor, date),
496
+ today: widget.highlightToday && sameDate(date, today),
497
+ }),
498
+ dayInfo.className,
499
+ CSS.mod(dayInfo.mod),
500
+ );
501
+ let dateInst = new Date(date);
502
+ days.push(
503
+ <td
504
+ key={i}
505
+ className={classNames}
506
+ style={CSS.parseStyle(dayInfo.style)}
507
+ data-year={dateInst.getFullYear()}
508
+ data-month={dateInst.getMonth() + 1}
509
+ data-date={dateInst.getDate()}
510
+ onMouseMove={unselectable ? null : this.handleMouseMove}
511
+ onMouseDown={unselectable ? null : this.handleMouseDown}
512
+ >
513
+ {date.getDate()}
514
+ </td>,
515
+ );
516
+ date.setDate(date.getDate() + 1);
517
+ }
518
+ weeks.push(
519
+ <tr key={weeks.length} className={CSS.element(baseClass, "week")}>
520
+ <td />
521
+ {days}
522
+ <td />
523
+ </tr>,
524
+ );
525
+ }
526
+
527
+ let culture = Culture.getDateTimeCulture();
528
+ let monthNames = culture.getMonthNames("long");
529
+ let dayNames = culture.getWeekdayNames("short").map((x) => x.substr(0, 2));
530
+ if (startWithMonday) dayNames = [...dayNames.slice(1), dayNames[0]];
531
+
532
+ return (
533
+ <div
534
+ className={data.classNames}
535
+ tabIndex={data.disabled || !data.focusable ? null : data.tabIndex || 0}
536
+ onKeyDown={(e) => this.handleKeyPress(e)}
537
+ onMouseDown={(e) => {
538
+ // prevent losing focus from the input field
539
+ if (!data.focusable) {
540
+ e.preventDefault();
541
+ }
542
+ e.stopPropagation();
543
+ }}
544
+ ref={(el) => {
545
+ this.el = el;
546
+ }}
547
+ onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))}
548
+ onMouseLeave={(e) => this.handleMouseLeave(e)}
549
+ onMouseEnter={(e) => this.handleMouseEnter(e)}
550
+ // onWheel={(e) => this.handleWheel(e)}
551
+ onFocus={(e) => this.handleFocus(e)}
552
+ onBlur={(e) => this.handleBlur(e)}
553
+ >
554
+ {this.state.activeView == "calendar" && (
555
+ <table>
556
+ <thead>
557
+ <tr key="h" className={CSS.element(baseClass, "header")}>
558
+ <td />
559
+ <td onClick={(e) => this.move(e, "y", -1)}>
560
+ <ForwardIcon className={CSS.element(baseClass, "icon-prev-year")} />
561
+ </td>
562
+ <td onClick={(e) => this.move(e, "m", -1)}>
563
+ <DropdownIcon className={CSS.element(baseClass, "icon-prev-month")} />
564
+ </td>
565
+ <th className={CSS.element(baseClass, "display")} colSpan="3">
566
+ {monthNames[month]}
567
+ <br />
568
+ <span
569
+ onClick={() => this.showYearDropdown()}
570
+ className={CSS.element(baseClass, "year-name")}
571
+ >
572
+ {year}
573
+ </span>
574
+ </th>
575
+ <td onClick={(e) => this.move(e, "m", +1)}>
576
+ <DropdownIcon className={CSS.element(baseClass, "icon-next-month")} />
577
+ </td>
578
+ <td onClick={(e) => this.move(e, "y", +1)}>
579
+ <ForwardIcon className={CSS.element(baseClass, "icon-next-year")} />
580
+ </td>
581
+ <td />
582
+ </tr>
583
+ <tr key="d" className={CSS.element(baseClass, "day-names")}>
584
+ <td />
585
+ {dayNames.map((name, i) => (
586
+ <th key={i}>{name}</th>
587
+ ))}
588
+ <td />
589
+ </tr>
590
+ </thead>
591
+ <tbody>{weeks}</tbody>
592
+ </table>
593
+ )}
594
+ {this.state.activeView == "calendar" && widget.showTodayButton && (
595
+ <div className={CSS.element(baseClass, "toolbar")}>
596
+ <button
597
+ className={CSS.expand(CSS.element(baseClass, "today-button"), CSS.block("button", "hollow"))}
598
+ data-year={today.getFullYear()}
599
+ data-month={today.getMonth() + 1}
600
+ data-date={today.getDate()}
601
+ onClick={(e) => {
602
+ this.handleMouseDown(e);
603
+ }}
604
+ >
605
+ {widget.todayButtonText}
606
+ </button>
607
+ </div>
608
+ )}
609
+
610
+ {this.state.activeView == "year-picker" && this.renderYearPicker()}
611
+ </div>
612
+ );
613
+ }
614
+ }
615
+
616
+ const readDate = (ds) => new Date(Number(ds.year), Number(ds.month) - 1, Number(ds.date));
617
+
618
+ Widget.alias("calendar", Calendar);