@startlibs/form 1.0.11 → 1.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.
Files changed (92) hide show
  1. package/lib/AutoComplete.mjs +550 -0
  2. package/lib/AutoComplete.mjs.map +1 -0
  3. package/lib/Checkbox.mjs +76 -0
  4. package/lib/Checkbox.mjs.map +1 -0
  5. package/lib/Combobox.mjs +544 -0
  6. package/lib/Combobox.mjs.map +1 -0
  7. package/lib/ComboboxWithCustom.mjs +77 -0
  8. package/lib/ComboboxWithCustom.mjs.map +1 -0
  9. package/lib/DatePicker.mjs +625 -0
  10. package/lib/DatePicker.mjs.map +1 -0
  11. package/lib/EditableBox.mjs +496 -0
  12. package/lib/EditableBox.mjs.map +1 -0
  13. package/lib/Errors.mjs +176 -0
  14. package/lib/Errors.mjs.map +1 -0
  15. package/lib/ExpandableBox.mjs +209 -0
  16. package/lib/ExpandableBox.mjs.map +1 -0
  17. package/lib/Field.mjs +65 -0
  18. package/lib/Field.mjs.map +1 -0
  19. package/lib/FieldBox.mjs +80 -0
  20. package/lib/FieldBox.mjs.map +1 -0
  21. package/lib/Form.mjs +322 -0
  22. package/lib/Form.mjs.map +1 -0
  23. package/lib/FormValue.mjs +111 -0
  24. package/lib/FormValue.mjs.map +1 -0
  25. package/lib/InputboxGroup.mjs +104 -0
  26. package/lib/InputboxGroup.mjs.map +1 -0
  27. package/lib/Radiobox.mjs +67 -0
  28. package/lib/Radiobox.mjs.map +1 -0
  29. package/lib/RichText.mjs +634 -0
  30. package/lib/RichText.mjs.map +1 -0
  31. package/lib/TextInput.mjs +359 -0
  32. package/lib/TextInput.mjs.map +1 -0
  33. package/lib/alt/checkbox/SimpleCheckbox.mjs +82 -0
  34. package/lib/alt/checkbox/SimpleCheckbox.mjs.map +1 -0
  35. package/lib/alt/checkbox/ToggleCheckbox.mjs +68 -0
  36. package/lib/alt/checkbox/ToggleCheckbox.mjs.map +1 -0
  37. package/lib/alt/radiobox/CleanTabRadiobox.mjs +44 -0
  38. package/lib/alt/radiobox/CleanTabRadiobox.mjs.map +1 -0
  39. package/lib/alt/radiobox/IconRadioBox.mjs +52 -0
  40. package/lib/alt/radiobox/IconRadioBox.mjs.map +1 -0
  41. package/lib/alt/radiobox/SimpleRadiobox.mjs +61 -0
  42. package/lib/alt/radiobox/SimpleRadiobox.mjs.map +1 -0
  43. package/lib/alt/radiobox/TabRadiobox.mjs +54 -0
  44. package/lib/alt/radiobox/TabRadiobox.mjs.map +1 -0
  45. package/lib/enhanceInput.mjs +131 -0
  46. package/lib/enhanceInput.mjs.map +1 -0
  47. package/lib/index.cjs +5345 -0
  48. package/lib/index.cjs.map +1 -0
  49. package/lib/index.mjs +28 -0
  50. package/lib/index.mjs.map +1 -0
  51. package/lib/useConfirmDialog.mjs +117 -0
  52. package/lib/useConfirmDialog.mjs.map +1 -0
  53. package/lib/useDraftConfirmation.mjs +56 -0
  54. package/lib/useDraftConfirmation.mjs.map +1 -0
  55. package/lib/validation.mjs +91 -0
  56. package/lib/validation.mjs.map +1 -0
  57. package/lib/withFormImageInput.mjs +281 -0
  58. package/lib/withFormImageInput.mjs.map +1 -0
  59. package/package.json +44 -25
  60. package/src/AutoComplete.jsx +642 -0
  61. package/src/Checkbox.jsx +45 -0
  62. package/src/Combobox.jsx +606 -0
  63. package/src/ComboboxWithCustom.jsx +68 -0
  64. package/src/DatePicker.jsx +733 -0
  65. package/src/EditableBox.jsx +502 -0
  66. package/src/Errors.jsx +174 -0
  67. package/src/ExpandableBox.jsx +207 -0
  68. package/src/Field.jsx +76 -0
  69. package/src/FieldBox.jsx +136 -0
  70. package/src/Form.jsx +291 -0
  71. package/src/FormValue.jsx +67 -0
  72. package/src/IconRadioBoxOldHubt.jsx +93 -0
  73. package/src/InputboxGroup.jsx +121 -0
  74. package/src/Radiobox.jsx +33 -0
  75. package/src/RichText.jsx +812 -0
  76. package/src/TextInput.jsx +519 -0
  77. package/src/alt/checkbox/SimpleCheckbox.jsx +162 -0
  78. package/src/alt/checkbox/ToggleCheckbox.jsx +143 -0
  79. package/src/alt/radiobox/CleanTabRadiobox.jsx +46 -0
  80. package/src/alt/radiobox/IconRadioBox.jsx +93 -0
  81. package/src/alt/radiobox/SimpleRadiobox.jsx +153 -0
  82. package/src/alt/radiobox/TabRadiobox.jsx +73 -0
  83. package/src/enhanceInput.jsx +107 -0
  84. package/src/index.js +27 -0
  85. package/src/useConfirmDialog.jsx +105 -0
  86. package/src/useDraftConfirmation.jsx +60 -0
  87. package/src/validation.js +92 -0
  88. package/src/withFormImageInput.jsx +297 -0
  89. package/.babelrc +0 -28
  90. package/lib/index.js +0 -6255
  91. package/rollup.config.js +0 -43
  92. package/yarn-error.log +0 -1607
@@ -0,0 +1,733 @@
1
+ import React, {forwardRef, useContext, useEffect, useRef, useState} from 'react'
2
+ import styled, {css} from 'styled-components'
3
+ import _ from 'lodash/fp'
4
+ import {StartlibsContext, useEnsureRef, usePopupToggle, usePrevious} from '@startlibs/core'
5
+ import {formatDate, formatDateToPattern, getColor, getNumberInputType, ifProp, media, parseDate} from '@startlibs/utils'
6
+ import {Icon} from '@startlibs/components'
7
+ import {Field} from './Field'
8
+ import {TextInput} from './TextInput'
9
+ import {enhanceInput, useInput} from './enhanceInput'
10
+ import {darken} from 'polished'
11
+ import {callIfFunction} from '@startlibs/utils'
12
+
13
+ export const CALENDAR_LEVEL_DAY = 'day'
14
+ export const CALENDAR_LEVEL_MONTH = 'month'
15
+ export const CALENDAR_LEVEL_YEAR = 'year'
16
+
17
+ const LEVELS = [CALENDAR_LEVEL_DAY, CALENDAR_LEVEL_MONTH, CALENDAR_LEVEL_YEAR]
18
+
19
+ const DatePickerContainer = styled.div`
20
+ width: 20rem;
21
+ position: absolute;
22
+ z-index: 100;
23
+ background-color: white;
24
+ border-radius: 5px;
25
+ border: 1px solid ${getColor('gray210')};
26
+ user-select: none;
27
+ box-shadow: 0 0 4px 0 rgba(0,0,0,0.2);
28
+ font-size: 13px;
29
+ text-align: center;
30
+ animation: 0.2s datePickerFadeInDown ease-out;
31
+ display: none;
32
+ @supports (flex-wrap: wrap) {
33
+ display: block;
34
+ }
35
+ input {
36
+ padding-right: 2.25rem;
37
+ ${media.mobile`
38
+ width: 2.5rem;
39
+ `}
40
+ }
41
+ @keyframes datePickerFadeInDown {
42
+ from {
43
+ opacity: 0;
44
+ transform: translateY(-10px);
45
+ pointer-events: none;
46
+ }
47
+ to {
48
+ opacity: 1;
49
+ transform: none;
50
+ pointer-events: none;
51
+ }
52
+ }
53
+ `
54
+
55
+ const CalendarHeader = styled.div`
56
+ display: flex;
57
+ padding: 0.5rem;
58
+ align-items: center;
59
+ `
60
+
61
+ const CalendarContentContainer = styled.div`
62
+ padding: 0 0.5rem 0.5rem 0.5rem;
63
+ display: flex;
64
+ flex-wrap: wrap;
65
+ justify-content: space-around;
66
+ `
67
+
68
+ const WeekHeader = styled(CalendarContentContainer)`
69
+ font-size: 12px;
70
+ font-weight: 600;
71
+ cursor: default;
72
+ span {
73
+ width: 2.5rem;
74
+ }
75
+ `
76
+
77
+ const CalendarClickable = styled.a`
78
+ :hover, :focus {
79
+ background-color: rgba(0,0,0,0.1);
80
+ cursor: pointer;
81
+ }
82
+ `
83
+
84
+ const TitleButton = styled(CalendarClickable)`
85
+ font-size: 16px;
86
+ font-weight: 600;
87
+ flex-grow: 1;
88
+ border-radius: 4px;
89
+ line-height: 3rem;
90
+ `
91
+
92
+ const NavigationButton = styled(CalendarClickable)`
93
+ font-size: 21px;
94
+ width: 3rem;
95
+ text-align: center;
96
+ border-radius: 4px;
97
+ height: 3rem;
98
+ ${Icon} {
99
+ line-height: 3rem;
100
+ }
101
+ `
102
+
103
+ const CalendarContent = styled(CalendarClickable)`
104
+ line-height: 2.25rem;
105
+ flex-grow: 1;
106
+ display: inline-block;
107
+ border-radius: 5px;
108
+ margin-top: 0.25rem;
109
+ margin-left: 0.25rem;
110
+ ${ifProp('isEdge', css`color: rgba(0,0,0,0.4);`)}
111
+ ${ifProp('isSelected', css`box-shadow: inset 0 0 0 1px ${getColor('gray210')};`)}
112
+ ${ifProp('isDisabled',
113
+ `pointer-events: none;
114
+ opacity: 0.5;`
115
+ )}
116
+ ${ifProp('isCurrent', css`
117
+ background: ${getColor('main')};
118
+ color: #FFF;
119
+ :hover {
120
+ background-color: ${props => darken(0.075, getColor('main')(props))};
121
+ }
122
+ ${ifProp('isSelected', 'box-shadow: none;')}
123
+ `)}
124
+ `
125
+
126
+ const DayItem = styled(CalendarContent)`
127
+ ${props => props.index % 7 === 0 && 'margin-left:0;'}
128
+ flex-basis: 12%;
129
+ `
130
+ const MonthItem = styled(CalendarContent)`
131
+ ${props => props.index % 3 === 0 && 'margin-left:0;'}
132
+ flex-basis: 30%;
133
+ `
134
+ const YearItem = styled(CalendarContent)`
135
+ ${props => props.index % 3 === 0 && 'margin-left:0;'}
136
+ flex-basis: 30%;
137
+ `
138
+
139
+ const DatePickerInputIcon = styled(Icon)`
140
+ position: absolute;
141
+ outline: none;
142
+ bottom: 0;
143
+ font-size: 21px;
144
+ line-height: 3rem;
145
+ right: 0;
146
+ width: 2rem;
147
+ cursor: pointer;
148
+ color: ${getColor('gray150')};
149
+ :hover {
150
+ color: ${getColor('main')};
151
+ }
152
+ display: none;
153
+ @supports (flex-wrap: wrap) {
154
+ display: block;
155
+ }
156
+ ${props => props.disabled && 'pointer-events: none;'}
157
+ ${media.mobile`
158
+ line-height: 42px;
159
+ width: 2.25rem;
160
+ `}
161
+ `
162
+
163
+ const ClearButton = styled(Icon)`
164
+ color: ${getColor('gray90')};
165
+ font-size: 12px;
166
+ position: absolute;
167
+ right: 40px;
168
+ bottom: 0;
169
+ transform: translateY(-50%);
170
+ cursor: pointer;
171
+ width: 1.5rem;
172
+ line-height: 1.5rem;
173
+ text-align: center;
174
+ border-radius: 50%;
175
+ background: rgba(0,0,0,0.1);
176
+ :hover {
177
+ background: #f7d3d4;
178
+ color: ${getColor('alert')};
179
+ }
180
+ `
181
+
182
+ const InputAndIconWrapper = styled.div`
183
+ position: relative;
184
+ `
185
+
186
+ const ShortcutsWrapper = styled.div`
187
+ display: flex;
188
+ flex-wrap: wrap;
189
+ gap: 0.5rem;
190
+ padding: 0.5rem 0.25rem 0;
191
+ color: ${getColor('gray150')};
192
+
193
+ .link {
194
+ cursor: pointer;
195
+ font-weight: 600;
196
+ text-decoration: underline;
197
+
198
+ ${media.mobile`
199
+ font-size: 13px;
200
+ `}
201
+
202
+ :hover {
203
+ color: ${getColor('main')};
204
+ }
205
+
206
+ ${ifProp('disabled', css`
207
+ pointer-events: none;
208
+ opacity: 0.5;
209
+ `)}
210
+ }
211
+ `
212
+
213
+
214
+ const KeyMapper = (level) => {
215
+ const upDownSpacing = level === CALENDAR_LEVEL_DAY ? 7 : 3
216
+ return {
217
+ 'ArrowLeft': -1,
218
+ 'ArrowRight': 1,
219
+ 'ArrowUp': -upDownSpacing,
220
+ 'ArrowDown': upDownSpacing
221
+ }
222
+ }
223
+
224
+ const getUTCDate = (year,month = 0,date = 1,hour = 0,min =0 ,sec =0 ,ms = 0) => new Date(Date.UTC(year,month,date,hour,min,sec,ms))
225
+ const getUTCDateBefore = (year,month = 0,date = 1,hour = 0,min =0 ,sec =0 ,ms = -1) => new Date(Date.UTC(year,month,date,hour,min,sec,ms))
226
+
227
+ export const DatePicker = forwardRef((
228
+ {
229
+ format = 'dd/MM/yyyy',
230
+ min = new Date(now().setUTCFullYear(now().getUTCFullYear() - 100)),
231
+ max = new Date(now().setUTCFullYear(now().getUTCFullYear() + 100)),
232
+ weekStartsMonday = false,
233
+ label, mandatory, tabIndex, helpText,
234
+ path,
235
+ timezoneOffset = 0,
236
+ placeholder,
237
+ onBlur,
238
+ raw,
239
+ disabled,
240
+ level,
241
+ daysFromNowShortcuts, // Uma array de números representando atalhos de dias a partir de agora, e.g. [0, 1, 3]
242
+ monthsFromNowShortcuts, // Uma array de números representando atalhos de meses a partir de agora, e.g. [0, 1, 3]
243
+ ...rest
244
+ },
245
+ maybeInputRef
246
+ ) => {
247
+
248
+ const inputRef = useEnsureRef(maybeInputRef)
249
+
250
+ const [formValue, setUnoffsetedFormValue, {hasErrors}] = useInput({path,...rest},raw)
251
+ const setFormValue = (newV) => setUnoffsetedFormValue(_.isFinite(newV) && newV !== -1 ? newV - (timezoneOffset * 36e5) : newV)
252
+
253
+ const popup = usePopupToggle()
254
+ const calendarElement = useRef()
255
+
256
+ const messages = useContext(StartlibsContext).dateMessages
257
+
258
+ const time = Number(formValue)
259
+ const offsetedTime = time + (timezoneOffset * 36e5)
260
+ const [textValue, setRawTextValue] = useState(!isNaN(time) ? formatDate(new Date(offsetedTime), format, messages) : '')
261
+ const prevValue = usePrevious(textValue)
262
+ const [hasFormatErrors, setHasFormatErrors] = useState(false)
263
+
264
+ useEffect(() => {
265
+ if (textValue && prevValue && textValue.length > prevValue.length+1) {
266
+ setTimeout(()=>inputRef.current.setSelectionRange(inputRef.current.value.length+1,inputRef.current.value.length+1),1);
267
+ }
268
+ },[textValue])
269
+
270
+ const isShortcutsEnabled = !!daysFromNowShortcuts && daysFromNowShortcuts.length > 0 || !!monthsFromNowShortcuts && monthsFromNowShortcuts.length > 0;
271
+
272
+ const setDateToNDaysFromNow = (daysFromNow) => {
273
+ const date = new Date(now())
274
+ date.setUTCDate(date.getUTCDate() + daysFromNow)
275
+ setFormValue(date.getTime())
276
+ updateText(date)
277
+ }
278
+
279
+ const setDateToNMonthsFromNow = (monthsFromNow) => {
280
+ const date = new Date(now())
281
+ date.setUTCMonth(date.getUTCMonth() + monthsFromNow)
282
+ setFormValue(date.getTime())
283
+ updateText(date)
284
+ }
285
+
286
+ const setTextValue = (textValue) => {
287
+ const value = formValue === -1 || isNaN(formValue) ? now() : new Date(Number(formValue))
288
+ const date = parseDate(textValue, value, format, messages)
289
+ if (isNaN(date.getTime())) {
290
+ const nextMonth = new Date(value)
291
+ const closestLeapYear = new Date(value)
292
+ nextMonth.setUTCMonth(value.getUTCMonth() + 1)
293
+ closestLeapYear.setUTCFullYear(getClosestNextLeapYear(value.getUTCFullYear()))
294
+ const nextMonthDate = parseDate(textValue, nextMonth, format, messages)
295
+ const closestLeapYearDate = parseDate(textValue, closestLeapYear, format, messages)
296
+ setRawTextValue(textValue)
297
+ setHasFormatErrors(isNaN(nextMonthDate.getTime()) && isNaN(closestLeapYearDate))
298
+ } else if (!inRange({min,max},date)) {
299
+ setRawTextValue(textValue)
300
+ setHasFormatErrors(true)
301
+ setFormValue(-1)
302
+ } else {
303
+ setRawTextValue(textValue)
304
+ setHasFormatErrors(false)
305
+ setFormValue(date.getTime())
306
+ }
307
+ }
308
+
309
+ const updateText = (date) => {
310
+ setTextValue(formatDate(date, format, messages))
311
+ setHasFormatErrors(false)
312
+ }
313
+
314
+ const setValue = (value) => {
315
+ setFormValue(value)
316
+ updateText(new Date(value))
317
+ }
318
+
319
+ const confirmValue = () => {
320
+ popup.close()
321
+ if (!textValue) {
322
+ setFormValue(undefined)
323
+ setHasFormatErrors(false)
324
+ } else {
325
+ const date = parseDate(textValue, new Date(formValue), format, messages, true)
326
+ if (isNaN(date.getTime())) {
327
+ setHasFormatErrors(true)
328
+ setFormValue(-1)
329
+ } else {
330
+ updateText(date)
331
+ }
332
+ }
333
+ }
334
+
335
+ const _onBlur = (e) => {
336
+ const currentTarget = e.currentTarget
337
+ setTimeout(() => {
338
+ if (!currentTarget.contains(document.activeElement)) {
339
+ confirmValue()
340
+ }
341
+ callIfFunction(onBlur)
342
+ }, 1)
343
+ }
344
+
345
+ const onKeyDown = (e) => {
346
+ const isConfirm = e.key === 'Enter'
347
+ if (Object.keys(KeyMapper()).indexOf(e.key) >= 0 || isConfirm) {
348
+ if (popup.isOpen) {
349
+ e.preventDefault()
350
+ calendarElement.current.keyDown(e)
351
+ }
352
+ }
353
+ if (e.key === 'Escape') {
354
+ e.stopPropagation()
355
+ popup.close()
356
+ const date = new Date(formValue)
357
+ if (!isNaN(date.getTime()))
358
+ updateText(date)
359
+ }
360
+ }
361
+
362
+ const refocus = (e) => inputRef.current.focus()
363
+
364
+ const onClick = e => {
365
+ if (disabled) return;
366
+ e.stopPropagation()
367
+ refocus()
368
+ }
369
+
370
+ const iconClick = e => {
371
+ e.stopPropagation()
372
+ if (popup.isOpen) {
373
+ refocus()
374
+ popup.close()
375
+ } else {
376
+ refocus()
377
+ }
378
+ }
379
+
380
+ const stopFocusPropagation = (e) => e.stopPropagation()
381
+
382
+ // const calendarValue = new Date(!isNaN(time) && formValue !== -1 ? offsetedTime : now())
383
+ const calendarValue = new Date(!isNaN(time) && formValue !== -1 ? offsetedTime : nowTimeZoneAdjusted(timezoneOffset))
384
+ calendarValue.setUTCHours(0, 0, 0)
385
+ return <Field helpText={helpText} label={label} mandatory={mandatory} onClick={onClick} onFocus={popup.open} onBlur={_onBlur}>
386
+ <InputAndIconWrapper>
387
+ <TextInput
388
+ raw
389
+ disabled={disabled}
390
+ type={getNumberInputType()}
391
+ tabIndex={tabIndex}
392
+ ref={inputRef}
393
+ constraint={formatDateToPattern(format)}
394
+ value={textValue}
395
+ setValue={setTextValue}
396
+ placeholder={placeholder}
397
+ hasErrors={hasErrors || hasFormatErrors}
398
+ onKeyDown={onKeyDown}
399
+ />
400
+ <DatePickerInputIcon tabIndex={-1} onFocus={stopFocusPropagation} disabled={disabled} onClick={iconClick} icon="calendar" className={isShortcutsEnabled ? "hasShortcuts" : ""} />
401
+ </InputAndIconWrapper>
402
+
403
+ {(daysFromNowShortcuts?.length > 0 || monthsFromNowShortcuts?.length > 0) ?
404
+ <ShortcutsWrapper>
405
+ <span>Em:</span>
406
+ {daysFromNowShortcuts?.map((daysFromNow) => (
407
+ <span class="link"
408
+ key={`days-from-now-${daysFromNow}`}
409
+ onClick={() => setDateToNDaysFromNow(daysFromNow)}
410
+ disabled={disabled}
411
+ >
412
+ {daysFromNow} dia{daysFromNow > 1 ? 's' : ''}
413
+ </span>
414
+ ))}
415
+
416
+ {monthsFromNowShortcuts?.map((monthsFromNow) => (
417
+ <span class="link"
418
+ key={`months-from-now${monthsFromNow}`}
419
+ onClick={() => setDateToNMonthsFromNow(monthsFromNow)}
420
+ disabled={disabled}
421
+ >
422
+ {monthsFromNow} {monthsFromNow > 1 ? 'meses' : 'mês'}
423
+ </span>
424
+ ))}
425
+ </ShortcutsWrapper>
426
+ : null}
427
+
428
+ {
429
+ popup.isOpen &&
430
+ <DatePickerCalendar
431
+ raw
432
+ min={min}
433
+ max={max}
434
+ level={level}
435
+ weekStartsMonday={weekStartsMonday}
436
+ messages={messages}
437
+ ref={calendarElement}
438
+ updateTransientValue={updateText}
439
+ refocus={refocus}
440
+ onSelectDay={popup.toggle}
441
+ setValue={setValue}
442
+ value={calendarValue.getTime()}
443
+ />
444
+ }
445
+ </Field>
446
+ })
447
+
448
+
449
+ const defaultProps = {
450
+ format: 'dd/MM/yyyy',
451
+ min: new Date(now().setUTCFullYear(now().getUTCFullYear() - 100)),
452
+ max: new Date(now().setUTCFullYear(now().getUTCFullYear() + 100)),
453
+ weekStartsMonday: false
454
+ }
455
+
456
+
457
+
458
+ class RawDatePickerCalendar extends React.Component {
459
+
460
+ static defaultProps = defaultProps
461
+
462
+ constructor(props) {
463
+ super(props)
464
+ this.state = {level: this.props.level || CALENDAR_LEVEL_DAY, currentDate: new Date(props.value || now())}
465
+ }
466
+
467
+ element
468
+
469
+ componentDidUpdate(prevProps) {
470
+ if (prevProps.value !== this.props.value) {
471
+ this.setState({
472
+ currentDate: new Date(this.props.value)
473
+ })
474
+ }
475
+ }
476
+
477
+ setCurrentDatePlus = (n, type) => () => {
478
+ const date = new Date(this.state.currentDate.getTime())
479
+ switch (type) {
480
+ case CALENDAR_LEVEL_DAY:
481
+ date.setUTCDate(date.getUTCDate() + n)
482
+ break
483
+ case CALENDAR_LEVEL_MONTH:
484
+ date.setUTCMonth(date.getUTCMonth() + n)
485
+ break
486
+ case CALENDAR_LEVEL_YEAR:
487
+ date.setUTCFullYear(date.getUTCFullYear() + n)
488
+ break
489
+ default:
490
+ break
491
+ }
492
+ this.props.updateTransientValue && this.props.updateTransientValue(date)
493
+ this.setState({currentDate: date})
494
+ }
495
+
496
+ up = () => {
497
+ this.setState({level: LEVELS[LEVELS.indexOf(this.state.level) + 1]})
498
+ }
499
+
500
+
501
+ keyDown = (e) => {
502
+ const {level, currentDate} = this.state
503
+ const keyMapper = KeyMapper(level)
504
+ if (keyMapper[e.key]) {
505
+ e.preventDefault()
506
+ this.setCurrentDatePlus(keyMapper[e.key], level)()
507
+ }
508
+ if (e.key === 'Enter') {
509
+ e.stopPropagation()
510
+ e.preventDefault()
511
+ this.handleChange(currentDate)
512
+ }
513
+ }
514
+
515
+ handleChange = (date) => {
516
+ this.props.setValue(date.getTime())
517
+ if (this.state.level === CALENDAR_LEVEL_DAY && this.props.onSelectDay) {
518
+ this.props.onSelectDay()
519
+ }
520
+ this.setState({level: LEVELS[Math.max(LEVELS.indexOf(this.state.level) - 1, 0)]})
521
+ }
522
+
523
+ getView = () => {
524
+ switch (this.state.level) {
525
+ case CALENDAR_LEVEL_DAY:
526
+ return <Calendar {...this.props} {...this.state} handleChange={this.handleChange}
527
+ changeCurrentDate={this.setCurrentDatePlus} up={this.up}/>
528
+ case CALENDAR_LEVEL_MONTH:
529
+ return <Months {...this.props} {...this.state} handleChange={this.handleChange}
530
+ changeCurrentDate={this.setCurrentDatePlus} up={this.up}/>
531
+ case CALENDAR_LEVEL_YEAR:
532
+ return <Years {...this.props} {...this.state} handleChange={this.handleChange}
533
+ changeCurrentDate={this.setCurrentDatePlus}/>
534
+ default:
535
+ return null
536
+ }
537
+ }
538
+
539
+ setRef = (e) => {
540
+ this.element = e
541
+ }
542
+
543
+ render() {
544
+ return (
545
+ <div ref={this.setRef} tabIndex={-1} onFocus={this.props.refocus} onKeyDown={this.keyDown}>
546
+ {this.getView()}
547
+ </div>
548
+ )
549
+
550
+ }
551
+ }
552
+ const DatePickerCalendar = enhanceInput(RawDatePickerCalendar)
553
+
554
+ function now() {
555
+ const dt = new Date()
556
+ dt.setUTCHours(0, 0, 0, 0)
557
+ return dt
558
+ }
559
+
560
+ function nowTimeZoneAdjusted(timezoneOffset){
561
+ const timezoneOffsetMilliseconds = timezoneOffset * 3600000; // 3600000 milliseconds in an hour
562
+ const dt = new Date()
563
+ dt.setTime(dt.getTime() + timezoneOffsetMilliseconds);
564
+ dt.setUTCHours(0, 0, 0, 0)
565
+ return dt;
566
+ };
567
+
568
+ function inRange(context, date) {
569
+ return context.min <= date && context.max >= date
570
+ }
571
+
572
+ const mapDays = (currentDate, dayOffset, fn) => {
573
+ const month = []
574
+ const iter = new Date(currentDate)
575
+ iter.setUTCDate(1)
576
+ let startDay = dayOffset + iter.getUTCDate() - iter.getUTCDay()
577
+ if (startDay > 1 && startDay < 10) {
578
+ startDay = startDay - 7
579
+ }
580
+ iter.setUTCDate(startDay)
581
+ for (let week = 0; week < 6; week++) {
582
+ for (let day = week * 7; day < (week * 7) + 7; day++) {
583
+ month.push(fn(iter, startDay + day, day))
584
+ iter.setUTCDate(iter.getUTCDate() + 1)
585
+ }
586
+ }
587
+ return month
588
+ }
589
+ const Calendar = ({messages, max, min, value, currentDate, weekStartsMonday, changeCurrentDate, handleChange, up}) => {
590
+ const currentMonth = currentDate.getUTCMonth()
591
+ const dayOffset = weekStartsMonday ? 1 : 0
592
+ const today = now().getTime()
593
+ const handleDateChange = (e) => {
594
+ const date = new Date(currentDate)
595
+ handleChange(new Date(date.setUTCDate(e.currentTarget.getAttribute('data-day'))))
596
+ }
597
+ const previousMonth = getUTCDateBefore(currentDate.getUTCFullYear(),currentDate.getUTCMonth())
598
+ const nextMonth = getUTCDate(currentDate.getUTCFullYear(),currentDate.getUTCMonth()+1)
599
+ return (
600
+ <DatePickerContainer>
601
+ <CalendarHeader>
602
+ <NavigationButton disabled={!inRange({min,max},previousMonth)} onClick={changeCurrentDate(-1, CALENDAR_LEVEL_MONTH)}>
603
+ <Icon icon="arrow-left-line"/>
604
+ </NavigationButton>
605
+ <TitleButton onClick={up}>{messages('month', currentMonth)} {currentDate.getUTCFullYear()}</TitleButton>
606
+ <NavigationButton disabled={!inRange({min,max},nextMonth)} onClick={changeCurrentDate(1, CALENDAR_LEVEL_MONTH)}>
607
+ <Icon icon="arrow-right-line"/>
608
+ </NavigationButton>
609
+ </CalendarHeader>
610
+ <WeekHeader>
611
+ {[0, 1, 2, 3, 4, 5, 6].map((day) =>
612
+ <span key={day}>{messages('day', (day + dayOffset) % 7).slice(0, 3)}</span>
613
+ )}
614
+ </WeekHeader>
615
+ <CalendarContentContainer>
616
+
617
+ {mapDays(currentDate, dayOffset, (date, day, index) =>
618
+ <DayItem
619
+ index={index}
620
+ key={date.getTime()}
621
+ onClick={handleDateChange}
622
+ data-day={day}
623
+ isEdge={date.getUTCMonth() !== currentMonth}
624
+ isCurrent={date.getTime() === currentDate.getTime()}
625
+ isSelected={date.getTime() === value}
626
+ isDisabled={!inRange({max, min}, date)}
627
+ isToday={date.getTime() === today}
628
+ >
629
+ {date.getUTCDate()}
630
+ </DayItem>
631
+ )}
632
+ </CalendarContentContainer>
633
+ </DatePickerContainer>
634
+ )
635
+ }
636
+
637
+ const getBeginDate = (year,month) => new Date(year,month,1,0,0,0,0)
638
+
639
+ const Months = ({currentDate, messages, value, changeCurrentDate, handleChange, up, min, max}) => {
640
+ const currentMonth = currentDate.getUTCMonth()
641
+ const sameYear = currentDate.getUTCFullYear() === new Date(value).getUTCFullYear()
642
+ const selectedMonth = sameYear ? currentDate.getUTCMonth() : undefined
643
+ const handleMonthChange = (e) => {
644
+ const date = new Date(currentDate)
645
+ handleChange(new Date(date.setUTCMonth(e.currentTarget.getAttribute('data-month'))))
646
+ }
647
+ return (
648
+ <DatePickerContainer>
649
+ <CalendarHeader>
650
+ <NavigationButton disabled={!inRange({min,max},getUTCDateBefore(currentDate.getUTCFullYear()))} onClick={changeCurrentDate(-1, CALENDAR_LEVEL_YEAR)}>
651
+ <Icon icon="arrow-left-line"/>
652
+ </NavigationButton>
653
+ <TitleButton onClick={up}>{currentDate.getUTCFullYear()}</TitleButton>
654
+ <NavigationButton disabled={!inRange({min,max},getUTCDate(currentDate.getUTCFullYear()+1))} onClick={changeCurrentDate(1, CALENDAR_LEVEL_YEAR)}>
655
+ <Icon icon="arrow-right-line"/>
656
+ </NavigationButton>
657
+ </CalendarHeader>
658
+ <CalendarContentContainer>
659
+ {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((month) =>
660
+ <MonthItem
661
+ disabled={!inRange({min,max},getUTCDate(currentDate.getUTCFullYear(),month)) && !inRange({min,max},getUTCDateBefore(currentDate.getUTCFullYear(),month+1))}
662
+ onClick={handleMonthChange}
663
+ key={month}
664
+ data-month={month}
665
+ index={month}
666
+ isCurrent={currentMonth === month}
667
+ isSelected={selectedMonth === month}
668
+ >
669
+ {messages('month', month).slice(0, 3)}
670
+ </MonthItem>
671
+ )}
672
+ </CalendarContentContainer>
673
+ </DatePickerContainer>
674
+ )
675
+ }
676
+
677
+ // Given the specified context, produces an HTML string
678
+ // representing months of the year.
679
+
680
+ function mapYears(currentDate, fn) {
681
+ const result = []
682
+ const year = currentDate.getUTCFullYear()
683
+ for (let i = 0; i < (4 * 3); i++) {
684
+ result.push(fn(year - (year % 10) - 1 + i, i < 1 || i > 10, i))
685
+ }
686
+ return result
687
+ }
688
+
689
+ const Years = ({value, currentDate, changeCurrentDate, handleChange, min, max}) => {
690
+ const currentYear = currentDate.getUTCFullYear()
691
+ const selectedYear = new Date(value).getUTCFullYear()
692
+ const handleYearChange = (e) => {
693
+ const date = new Date(currentDate)
694
+ handleChange(new Date(date.setUTCFullYear(e.currentTarget.getAttribute('data-year'))))
695
+ }
696
+ return (
697
+ <DatePickerContainer>
698
+ <CalendarHeader>
699
+ <NavigationButton disabled={!inRange({min,max},getUTCDateBefore(currentDate.getUTCFullYear()))} onClick={changeCurrentDate(-10, CALENDAR_LEVEL_YEAR)}>
700
+ <Icon icon="arrow-left-line"/>
701
+ </NavigationButton>
702
+ <TitleButton>{currentYear - (currentYear % 10)}-{currentYear - (currentYear % 10) + 9}</TitleButton>
703
+ <NavigationButton disabled={!inRange({min,max},getUTCDate(currentDate.getUTCFullYear()+10))} onClick={changeCurrentDate(10, CALENDAR_LEVEL_YEAR)}>
704
+ <Icon icon="arrow-right-line"/>
705
+ </NavigationButton>
706
+ </CalendarHeader>
707
+ <CalendarContentContainer>
708
+ {mapYears(currentDate, (year, outside, i) =>
709
+ <YearItem
710
+ key={year}
711
+ onClick={handleYearChange}
712
+ disabled={!inRange({min,max},getUTCDate(year)) && !inRange({min,max},getUTCDateBefore(year+1))}
713
+ data-year={year}
714
+ index={i}
715
+ isCurrent={year === currentYear}
716
+ isSelected={year === selectedYear}
717
+ isEdge={outside}
718
+ >
719
+ {year}
720
+ </YearItem>
721
+ )}
722
+ </CalendarContentContainer>
723
+ </DatePickerContainer>
724
+ )
725
+ }
726
+
727
+ function getClosestNextLeapYear(year) {
728
+ let y = isNaN(year) ? now().getUTCFullYear() : year
729
+ while (!((y % 100 === 0) ? (y % 400 === 0) : (y % 4 === 0))) {
730
+ y++
731
+ }
732
+ return y
733
+ }