@contentful/field-editor-date 1.3.5 → 1.5.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/dist/cjs/DateEditor.js +186 -0
- package/dist/cjs/DatepickerInput.js +92 -0
- package/dist/cjs/DatepickerInput.spec.js +110 -0
- package/dist/cjs/TimepickerInput.js +132 -0
- package/dist/cjs/TimezonePickerInput.js +32 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/types.js +4 -0
- package/dist/cjs/utils/data.spec.js +56 -0
- package/dist/cjs/utils/date.js +116 -0
- package/dist/cjs/utils/zoneOffsets.js +61 -0
- package/dist/esm/DateEditor.js +132 -0
- package/dist/esm/DatepickerInput.js +38 -0
- package/dist/esm/DatepickerInput.spec.js +62 -0
- package/dist/esm/TimepickerInput.js +78 -0
- package/dist/esm/TimezonePickerInput.js +17 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils/data.spec.js +47 -0
- package/dist/esm/utils/date.js +87 -0
- package/dist/esm/utils/zoneOffsets.js +43 -0
- package/dist/{DateEditor.d.ts → types/DateEditor.d.ts} +29 -29
- package/dist/{DatepickerInput.d.ts → types/DatepickerInput.d.ts} +8 -8
- package/dist/types/DatepickerInput.spec.d.ts +1 -0
- package/dist/{TimepickerInput.d.ts → types/TimepickerInput.d.ts} +12 -12
- package/dist/{TimezonePickerInput.d.ts → types/TimezonePickerInput.d.ts} +7 -7
- package/dist/{index.d.ts → types/index.d.ts} +2 -2
- package/dist/types/types.d.ts +9 -0
- package/dist/types/utils/data.spec.d.ts +1 -0
- package/dist/{utils → types/utils}/date.d.ts +27 -27
- package/dist/{utils → types/utils}/zoneOffsets.d.ts +2 -2
- package/package.json +28 -14
- package/CHANGELOG.md +0 -288
- package/dist/field-editor-date.cjs.development.js +0 -463
- package/dist/field-editor-date.cjs.development.js.map +0 -1
- package/dist/field-editor-date.cjs.production.min.js +0 -2
- package/dist/field-editor-date.cjs.production.min.js.map +0 -1
- package/dist/field-editor-date.esm.js +0 -455
- package/dist/field-editor-date.esm.js.map +0 -1
- package/dist/index.js +0 -8
- package/dist/types.d.ts +0 -9
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
buildFieldValue: function() {
|
|
13
|
+
return buildFieldValue;
|
|
14
|
+
},
|
|
15
|
+
getDefaultAMPM: function() {
|
|
16
|
+
return getDefaultAMPM;
|
|
17
|
+
},
|
|
18
|
+
getDefaultUtcOffset: function() {
|
|
19
|
+
return getDefaultUtcOffset;
|
|
20
|
+
},
|
|
21
|
+
userInputFromDatetime: function() {
|
|
22
|
+
return userInputFromDatetime;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const _moment = _interop_require_default(require("moment"));
|
|
26
|
+
function _interop_require_default(obj) {
|
|
27
|
+
return obj && obj.__esModule ? obj : {
|
|
28
|
+
default: obj
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const ZONE_RX = /(Z|[+-]\d{2}[:+]?\d{2})$/;
|
|
32
|
+
function startOfToday(format) {
|
|
33
|
+
return (0, _moment.default)().set({
|
|
34
|
+
hours: 0,
|
|
35
|
+
minutes: 0
|
|
36
|
+
}).format(format);
|
|
37
|
+
}
|
|
38
|
+
function fieldValueToMoment(datetimeString) {
|
|
39
|
+
if (!datetimeString) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const datetime = (0, _moment.default)(datetimeString);
|
|
43
|
+
if (ZONE_RX.test(datetimeString)) {
|
|
44
|
+
datetime.utcOffset(datetimeString);
|
|
45
|
+
}
|
|
46
|
+
return datetime;
|
|
47
|
+
}
|
|
48
|
+
function timeFromUserInput(input) {
|
|
49
|
+
const timeInput = input.time || '00:00';
|
|
50
|
+
return _moment.default.utc(timeInput + '!' + input.ampm, 'HH:mm!A');
|
|
51
|
+
}
|
|
52
|
+
function datetimeFromUserInput(input) {
|
|
53
|
+
if (!input.date) {
|
|
54
|
+
return {
|
|
55
|
+
valid: null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const time = timeFromUserInput(input);
|
|
59
|
+
const date = _moment.default.parseZone(input.utcOffset, 'Z').set(input.date.toObject()).set({
|
|
60
|
+
hours: time.hours(),
|
|
61
|
+
minutes: time.minutes()
|
|
62
|
+
});
|
|
63
|
+
if (date.isValid()) {
|
|
64
|
+
return {
|
|
65
|
+
valid: date
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
return {
|
|
69
|
+
invalid: true,
|
|
70
|
+
valid: null
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function buildFieldValue({ data , usesTime , usesTimezone }) {
|
|
75
|
+
const date = datetimeFromUserInput(data);
|
|
76
|
+
if (date.invalid) {
|
|
77
|
+
return {
|
|
78
|
+
invalid: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
let format;
|
|
82
|
+
if (usesTimezone) {
|
|
83
|
+
format = 'YYYY-MM-DDTHH:mmZ';
|
|
84
|
+
} else if (usesTime) {
|
|
85
|
+
format = 'YYYY-MM-DDTHH:mm';
|
|
86
|
+
} else {
|
|
87
|
+
format = 'YYYY-MM-DD';
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
valid: date?.valid ? date.valid.format(format) : null,
|
|
91
|
+
invalid: false
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function getDefaultAMPM() {
|
|
95
|
+
return 'AM';
|
|
96
|
+
}
|
|
97
|
+
function getDefaultUtcOffset() {
|
|
98
|
+
return startOfToday('Z');
|
|
99
|
+
}
|
|
100
|
+
function userInputFromDatetime({ value , uses12hClock }) {
|
|
101
|
+
const datetime = fieldValueToMoment(value);
|
|
102
|
+
if (datetime) {
|
|
103
|
+
const timeFormat = uses12hClock ? 'hh:mm' : 'HH:mm';
|
|
104
|
+
return {
|
|
105
|
+
date: datetime,
|
|
106
|
+
time: datetime.format(timeFormat),
|
|
107
|
+
ampm: datetime.format('A'),
|
|
108
|
+
utcOffset: datetime.format('Z')
|
|
109
|
+
};
|
|
110
|
+
} else {
|
|
111
|
+
return {
|
|
112
|
+
ampm: getDefaultAMPM(),
|
|
113
|
+
utcOffset: getDefaultUtcOffset()
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
defaultZoneOffset: function() {
|
|
13
|
+
return defaultZoneOffset;
|
|
14
|
+
},
|
|
15
|
+
zoneOffsets: function() {
|
|
16
|
+
return zoneOffsets;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const defaultZoneOffset = '+00:00';
|
|
20
|
+
const zoneOffsets = [
|
|
21
|
+
'-12:00',
|
|
22
|
+
'-11:00',
|
|
23
|
+
'-10:00',
|
|
24
|
+
'-09:30',
|
|
25
|
+
'-09:00',
|
|
26
|
+
'-08:00',
|
|
27
|
+
'-07:00',
|
|
28
|
+
'-06:00',
|
|
29
|
+
'-05:00',
|
|
30
|
+
'-04:30',
|
|
31
|
+
'-04:00',
|
|
32
|
+
'-03:30',
|
|
33
|
+
'-03:00',
|
|
34
|
+
'-02:00',
|
|
35
|
+
'-01:00',
|
|
36
|
+
'+00:00',
|
|
37
|
+
'+01:00',
|
|
38
|
+
'+02:00',
|
|
39
|
+
'+03:00',
|
|
40
|
+
'+03:30',
|
|
41
|
+
'+04:00',
|
|
42
|
+
'+04:30',
|
|
43
|
+
'+05:00',
|
|
44
|
+
'+05:30',
|
|
45
|
+
'+05:45',
|
|
46
|
+
'+06:00',
|
|
47
|
+
'+06:30',
|
|
48
|
+
'+07:00',
|
|
49
|
+
'+08:00',
|
|
50
|
+
'+08:45',
|
|
51
|
+
'+09:00',
|
|
52
|
+
'+09:30',
|
|
53
|
+
'+10:00',
|
|
54
|
+
'+10:30',
|
|
55
|
+
'+11:00',
|
|
56
|
+
'+11:30',
|
|
57
|
+
'+12:00',
|
|
58
|
+
'+12:45',
|
|
59
|
+
'+13:00',
|
|
60
|
+
'+14:00'
|
|
61
|
+
];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TextLink } from '@contentful/f36-components';
|
|
3
|
+
import tokens from '@contentful/f36-tokens';
|
|
4
|
+
import { FieldConnector } from '@contentful/field-editor-shared';
|
|
5
|
+
import { css } from 'emotion';
|
|
6
|
+
import { DatepickerInput } from './DatepickerInput';
|
|
7
|
+
import { TimepickerInput } from './TimepickerInput';
|
|
8
|
+
import { TimezonepickerInput } from './TimezonePickerInput';
|
|
9
|
+
import { userInputFromDatetime, buildFieldValue, getDefaultAMPM, getDefaultUtcOffset } from './utils/date';
|
|
10
|
+
const styles = {
|
|
11
|
+
root: css({
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center'
|
|
14
|
+
}),
|
|
15
|
+
separator: css({
|
|
16
|
+
marginLeft: tokens.spacingM
|
|
17
|
+
})
|
|
18
|
+
};
|
|
19
|
+
function useEffectWithoutFirstRender(callback, deps) {
|
|
20
|
+
const isFirstRun = React.useRef(true);
|
|
21
|
+
React.useEffect(()=>{
|
|
22
|
+
if (isFirstRun.current) {
|
|
23
|
+
isFirstRun.current = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
callback();
|
|
27
|
+
}, deps);
|
|
28
|
+
}
|
|
29
|
+
function DateEditorContainer({ initialValue , usesTime , usesTimezone , uses12hClock , disabled , hasClear , onChange }) {
|
|
30
|
+
const [value, setValue] = React.useState(()=>initialValue);
|
|
31
|
+
useEffectWithoutFirstRender(()=>{
|
|
32
|
+
onChange(value);
|
|
33
|
+
}, [
|
|
34
|
+
value
|
|
35
|
+
]);
|
|
36
|
+
return React.createElement("div", {
|
|
37
|
+
"data-test-id": "date-editor",
|
|
38
|
+
className: styles.root
|
|
39
|
+
}, React.createElement(DatepickerInput, {
|
|
40
|
+
disabled: disabled,
|
|
41
|
+
value: value.date,
|
|
42
|
+
onChange: (date)=>{
|
|
43
|
+
setValue((value)=>({
|
|
44
|
+
...value,
|
|
45
|
+
date
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}), usesTime && React.createElement(React.Fragment, null, React.createElement("div", {
|
|
49
|
+
className: styles.separator
|
|
50
|
+
}), React.createElement(TimepickerInput, {
|
|
51
|
+
disabled: disabled,
|
|
52
|
+
time: value.time,
|
|
53
|
+
ampm: value.ampm,
|
|
54
|
+
onChange: ({ time , ampm })=>{
|
|
55
|
+
setValue((value)=>({
|
|
56
|
+
...value,
|
|
57
|
+
time,
|
|
58
|
+
ampm
|
|
59
|
+
}));
|
|
60
|
+
},
|
|
61
|
+
uses12hClock: uses12hClock
|
|
62
|
+
})), usesTimezone && React.createElement(React.Fragment, null, React.createElement("div", {
|
|
63
|
+
className: styles.separator
|
|
64
|
+
}), React.createElement(TimezonepickerInput, {
|
|
65
|
+
disabled: disabled,
|
|
66
|
+
value: value.utcOffset,
|
|
67
|
+
onChange: (utcOffset)=>{
|
|
68
|
+
setValue((value)=>({
|
|
69
|
+
...value,
|
|
70
|
+
utcOffset
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
})), hasClear && React.createElement(React.Fragment, null, React.createElement("div", {
|
|
74
|
+
className: styles.separator
|
|
75
|
+
}), React.createElement(TextLink, {
|
|
76
|
+
as: "button",
|
|
77
|
+
isDisabled: disabled,
|
|
78
|
+
testId: "date-clear",
|
|
79
|
+
onClick: ()=>{
|
|
80
|
+
setValue({
|
|
81
|
+
date: undefined,
|
|
82
|
+
time: undefined,
|
|
83
|
+
ampm: getDefaultAMPM(),
|
|
84
|
+
utcOffset: getDefaultUtcOffset()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}, "Clear")));
|
|
88
|
+
}
|
|
89
|
+
export function DateEditor(props) {
|
|
90
|
+
const { field , parameters } = props;
|
|
91
|
+
const formatParam = parameters?.instance?.format ?? 'timeZ';
|
|
92
|
+
const ampmParam = parameters?.instance?.ampm ?? '24';
|
|
93
|
+
const usesTime = formatParam !== 'dateonly';
|
|
94
|
+
const usesTimezone = formatParam === 'timeZ';
|
|
95
|
+
const uses12hClock = ampmParam === '12';
|
|
96
|
+
return React.createElement(FieldConnector, {
|
|
97
|
+
field: field,
|
|
98
|
+
isInitiallyDisabled: props.isInitiallyDisabled,
|
|
99
|
+
isDisabled: props.isDisabled,
|
|
100
|
+
throttle: 0
|
|
101
|
+
}, ({ value , disabled , setValue , externalReset })=>{
|
|
102
|
+
const datetimeValue = userInputFromDatetime({
|
|
103
|
+
value,
|
|
104
|
+
uses12hClock
|
|
105
|
+
});
|
|
106
|
+
return React.createElement(DateEditorContainer, {
|
|
107
|
+
initialValue: datetimeValue,
|
|
108
|
+
uses12hClock: uses12hClock,
|
|
109
|
+
usesTimezone: usesTimezone,
|
|
110
|
+
usesTime: usesTime,
|
|
111
|
+
disabled: disabled,
|
|
112
|
+
hasClear: Boolean(value),
|
|
113
|
+
onChange: (data)=>{
|
|
114
|
+
const fieldValue = buildFieldValue({
|
|
115
|
+
data,
|
|
116
|
+
usesTime,
|
|
117
|
+
usesTimezone
|
|
118
|
+
});
|
|
119
|
+
if (fieldValue.invalid) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (Boolean(value) || !value && Boolean(fieldValue.valid)) {
|
|
123
|
+
setValue(fieldValue.valid);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
key: `date-container-${externalReset}`
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
DateEditor.defaultProps = {
|
|
131
|
+
isInitiallyDisabled: true
|
|
132
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Datepicker } from '@contentful/f36-components';
|
|
3
|
+
import { css } from 'emotion';
|
|
4
|
+
import moment from 'moment';
|
|
5
|
+
const YEAR_RANGE = 100;
|
|
6
|
+
const styles = {
|
|
7
|
+
root: css({
|
|
8
|
+
maxWidth: '270px'
|
|
9
|
+
})
|
|
10
|
+
};
|
|
11
|
+
export const DatepickerInput = (props)=>{
|
|
12
|
+
const [fromDate, toDate] = useMemo(()=>{
|
|
13
|
+
const fromDate = new Date();
|
|
14
|
+
fromDate.setFullYear(fromDate.getFullYear() - YEAR_RANGE);
|
|
15
|
+
const toDate = new Date();
|
|
16
|
+
toDate.setFullYear(toDate.getFullYear() + YEAR_RANGE);
|
|
17
|
+
return [
|
|
18
|
+
fromDate,
|
|
19
|
+
toDate
|
|
20
|
+
];
|
|
21
|
+
}, []);
|
|
22
|
+
const dateObj = props.value?.toObject();
|
|
23
|
+
const selectedDate = dateObj ? new Date(dateObj.years, dateObj.months, dateObj.date) : undefined;
|
|
24
|
+
return React.createElement(Datepicker, {
|
|
25
|
+
className: styles.root,
|
|
26
|
+
selected: selectedDate,
|
|
27
|
+
onSelect: (day)=>{
|
|
28
|
+
const momentDay = day ? moment(day) : undefined;
|
|
29
|
+
props.onChange(momentDay);
|
|
30
|
+
},
|
|
31
|
+
inputProps: {
|
|
32
|
+
isDisabled: props.disabled,
|
|
33
|
+
placeholder: ''
|
|
34
|
+
},
|
|
35
|
+
fromDate: fromDate,
|
|
36
|
+
toDate: toDate
|
|
37
|
+
});
|
|
38
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
import { configure, render, screen } from '@testing-library/react';
|
|
4
|
+
import timezonedDate from 'timezoned-date';
|
|
5
|
+
import { DatepickerInput } from './DatepickerInput';
|
|
6
|
+
import { userInputFromDatetime } from './utils/date';
|
|
7
|
+
configure({
|
|
8
|
+
testIdAttribute: 'data-test-id'
|
|
9
|
+
});
|
|
10
|
+
let originalDate;
|
|
11
|
+
beforeEach(()=>{
|
|
12
|
+
originalDate = global.Date;
|
|
13
|
+
});
|
|
14
|
+
afterEach(()=>{
|
|
15
|
+
global.Date = originalDate;
|
|
16
|
+
});
|
|
17
|
+
const renderDatepicker = (dateString)=>{
|
|
18
|
+
const { date } = userInputFromDatetime({
|
|
19
|
+
value: dateString,
|
|
20
|
+
uses12hClock: false
|
|
21
|
+
});
|
|
22
|
+
return render(React.createElement(DatepickerInput, {
|
|
23
|
+
value: date,
|
|
24
|
+
onChange: jest.fn()
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
27
|
+
describe('Date: DatepickerInput', function() {
|
|
28
|
+
describe('renders correct date, when the system UTC offset is +02:00 and the one in the props.value is', ()=>{
|
|
29
|
+
it('+04:00', async ()=>{
|
|
30
|
+
global.Date = timezonedDate.makeConstructor(120);
|
|
31
|
+
renderDatepicker('2022-09-22T00:00+04:00');
|
|
32
|
+
expect(screen.getByTestId('cf-ui-datepicker-input')).toHaveValue('22 Sep 2022');
|
|
33
|
+
});
|
|
34
|
+
it('+10:00', async ()=>{
|
|
35
|
+
global.Date = timezonedDate.makeConstructor(120);
|
|
36
|
+
renderDatepicker('2022-09-22T00:00+10:00');
|
|
37
|
+
expect(screen.getByTestId('cf-ui-datepicker-input')).toHaveValue('22 Sep 2022');
|
|
38
|
+
});
|
|
39
|
+
it('-08:00', async ()=>{
|
|
40
|
+
global.Date = timezonedDate.makeConstructor(120);
|
|
41
|
+
renderDatepicker('2022-09-22T00:00-08:00');
|
|
42
|
+
expect(screen.getByTestId('cf-ui-datepicker-input')).toHaveValue('22 Sep 2022');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('renders correct date, when the system UTC offset is -06:00 and the one in the props.value is', ()=>{
|
|
46
|
+
it('+02:00', async ()=>{
|
|
47
|
+
global.Date = timezonedDate.makeConstructor(-360);
|
|
48
|
+
renderDatepicker('2022-09-22T00:00+02:00');
|
|
49
|
+
expect(screen.getByTestId('cf-ui-datepicker-input')).toHaveValue('22 Sep 2022');
|
|
50
|
+
});
|
|
51
|
+
it('+10:00', async ()=>{
|
|
52
|
+
global.Date = timezonedDate.makeConstructor(-360);
|
|
53
|
+
renderDatepicker('2022-09-22T00:00+10:00');
|
|
54
|
+
expect(screen.getByTestId('cf-ui-datepicker-input')).toHaveValue('22 Sep 2022');
|
|
55
|
+
});
|
|
56
|
+
it('-08:00', async ()=>{
|
|
57
|
+
global.Date = timezonedDate.makeConstructor(-360);
|
|
58
|
+
renderDatepicker('2022-09-22T00:00-08:00');
|
|
59
|
+
expect(screen.getByTestId('cf-ui-datepicker-input')).toHaveValue('22 Sep 2022');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { TextInput, Flex } from '@contentful/f36-components';
|
|
3
|
+
import { css } from 'emotion';
|
|
4
|
+
import moment from 'moment';
|
|
5
|
+
const validInputFormats = [
|
|
6
|
+
'hh:mm a',
|
|
7
|
+
'hh:mm A',
|
|
8
|
+
'h:mm a',
|
|
9
|
+
'h:mm A',
|
|
10
|
+
'hh:mm',
|
|
11
|
+
'k:mm',
|
|
12
|
+
'kk:mm',
|
|
13
|
+
'h a',
|
|
14
|
+
'h A',
|
|
15
|
+
'h',
|
|
16
|
+
'hh',
|
|
17
|
+
'HH'
|
|
18
|
+
];
|
|
19
|
+
function parseRawInput(raw) {
|
|
20
|
+
let time = null;
|
|
21
|
+
for(let i = 0; i < validInputFormats.length; i++){
|
|
22
|
+
const date = moment(raw, validInputFormats[i]);
|
|
23
|
+
if (date.isValid()) {
|
|
24
|
+
time = date;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return time;
|
|
29
|
+
}
|
|
30
|
+
const getDefaultTime = ()=>{
|
|
31
|
+
return moment(`12:00 AM`, 'hh:mm A');
|
|
32
|
+
};
|
|
33
|
+
const formatToString = (uses12hClock, value)=>{
|
|
34
|
+
return uses12hClock ? value.format('hh:mm A') : value.format('HH:mm');
|
|
35
|
+
};
|
|
36
|
+
export const TimepickerInput = ({ disabled , uses12hClock , time ='12:00' , ampm ='AM' , onChange })=>{
|
|
37
|
+
const [selectedTime, setSelectedTime] = useState(()=>{
|
|
38
|
+
return formatToString(uses12hClock, getDefaultTime());
|
|
39
|
+
});
|
|
40
|
+
useEffect(()=>{
|
|
41
|
+
setSelectedTime(formatToString(uses12hClock, moment(`${time} ${ampm}`, 'hh:mm A')));
|
|
42
|
+
}, [
|
|
43
|
+
time,
|
|
44
|
+
ampm,
|
|
45
|
+
uses12hClock
|
|
46
|
+
]);
|
|
47
|
+
const handleChange = useCallback((e)=>{
|
|
48
|
+
setSelectedTime(e.currentTarget.value);
|
|
49
|
+
}, []);
|
|
50
|
+
const handleFocus = useCallback((e)=>{
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.target.select();
|
|
53
|
+
}, []);
|
|
54
|
+
const handleBlur = ()=>{
|
|
55
|
+
const parsedTime = parseRawInput(selectedTime);
|
|
56
|
+
const value = parsedTime ?? getDefaultTime();
|
|
57
|
+
setSelectedTime(formatToString(uses12hClock, value));
|
|
58
|
+
onChange({
|
|
59
|
+
time: value.format('hh:mm'),
|
|
60
|
+
ampm: value.format('A')
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
return React.createElement(Flex, {
|
|
64
|
+
className: css({
|
|
65
|
+
width: '145px'
|
|
66
|
+
})
|
|
67
|
+
}, React.createElement(TextInput, {
|
|
68
|
+
"aria-label": "Select time",
|
|
69
|
+
placeholder: uses12hClock ? '12:00 AM' : '00:00',
|
|
70
|
+
"date-time-type": uses12hClock ? '12' : '24',
|
|
71
|
+
testId: "time-input",
|
|
72
|
+
value: selectedTime,
|
|
73
|
+
isDisabled: disabled,
|
|
74
|
+
onFocus: handleFocus,
|
|
75
|
+
onBlur: handleBlur,
|
|
76
|
+
onChange: handleChange
|
|
77
|
+
}));
|
|
78
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Select } from '@contentful/f36-components';
|
|
3
|
+
import { zoneOffsets, defaultZoneOffset } from './utils/zoneOffsets';
|
|
4
|
+
export const TimezonepickerInput = ({ disabled , onChange , value =defaultZoneOffset })=>{
|
|
5
|
+
return React.createElement(Select, {
|
|
6
|
+
"aria-label": "Select timezone",
|
|
7
|
+
testId: "timezone-input",
|
|
8
|
+
value: value,
|
|
9
|
+
isDisabled: disabled,
|
|
10
|
+
onChange: (e)=>{
|
|
11
|
+
onChange(e.currentTarget.value);
|
|
12
|
+
}
|
|
13
|
+
}, zoneOffsets.map((offset)=>React.createElement(Select.Option, {
|
|
14
|
+
key: offset,
|
|
15
|
+
value: offset
|
|
16
|
+
}, "UTC", offset)));
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { buildFieldValue } from './date';
|
|
3
|
+
describe('date utils', ()=>{
|
|
4
|
+
describe('buildFieldValue', ()=>{
|
|
5
|
+
it('should work properly', ()=>{
|
|
6
|
+
expect(buildFieldValue({
|
|
7
|
+
data: {
|
|
8
|
+
date: moment('2018-02-02'),
|
|
9
|
+
time: '05:00',
|
|
10
|
+
ampm: 'PM',
|
|
11
|
+
utcOffset: '+03:00'
|
|
12
|
+
},
|
|
13
|
+
usesTimezone: true,
|
|
14
|
+
usesTime: true
|
|
15
|
+
})).toEqual({
|
|
16
|
+
invalid: false,
|
|
17
|
+
valid: '2018-02-02T17:00+03:00'
|
|
18
|
+
});
|
|
19
|
+
expect(buildFieldValue({
|
|
20
|
+
data: {
|
|
21
|
+
date: moment('2015-01-14'),
|
|
22
|
+
time: '05:00',
|
|
23
|
+
ampm: 'AM',
|
|
24
|
+
utcOffset: '-05:00'
|
|
25
|
+
},
|
|
26
|
+
usesTimezone: true,
|
|
27
|
+
usesTime: true
|
|
28
|
+
})).toEqual({
|
|
29
|
+
invalid: false,
|
|
30
|
+
valid: '2015-01-14T05:00-05:00'
|
|
31
|
+
});
|
|
32
|
+
expect(buildFieldValue({
|
|
33
|
+
data: {
|
|
34
|
+
date: moment('2015-01-14'),
|
|
35
|
+
time: '17:00',
|
|
36
|
+
ampm: 'PM',
|
|
37
|
+
utcOffset: '-05:00'
|
|
38
|
+
},
|
|
39
|
+
usesTimezone: true,
|
|
40
|
+
usesTime: true
|
|
41
|
+
})).toEqual({
|
|
42
|
+
invalid: false,
|
|
43
|
+
valid: '2015-01-14T17:00-05:00'
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
const ZONE_RX = /(Z|[+-]\d{2}[:+]?\d{2})$/;
|
|
3
|
+
function startOfToday(format) {
|
|
4
|
+
return moment().set({
|
|
5
|
+
hours: 0,
|
|
6
|
+
minutes: 0
|
|
7
|
+
}).format(format);
|
|
8
|
+
}
|
|
9
|
+
function fieldValueToMoment(datetimeString) {
|
|
10
|
+
if (!datetimeString) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const datetime = moment(datetimeString);
|
|
14
|
+
if (ZONE_RX.test(datetimeString)) {
|
|
15
|
+
datetime.utcOffset(datetimeString);
|
|
16
|
+
}
|
|
17
|
+
return datetime;
|
|
18
|
+
}
|
|
19
|
+
function timeFromUserInput(input) {
|
|
20
|
+
const timeInput = input.time || '00:00';
|
|
21
|
+
return moment.utc(timeInput + '!' + input.ampm, 'HH:mm!A');
|
|
22
|
+
}
|
|
23
|
+
function datetimeFromUserInput(input) {
|
|
24
|
+
if (!input.date) {
|
|
25
|
+
return {
|
|
26
|
+
valid: null
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const time = timeFromUserInput(input);
|
|
30
|
+
const date = moment.parseZone(input.utcOffset, 'Z').set(input.date.toObject()).set({
|
|
31
|
+
hours: time.hours(),
|
|
32
|
+
minutes: time.minutes()
|
|
33
|
+
});
|
|
34
|
+
if (date.isValid()) {
|
|
35
|
+
return {
|
|
36
|
+
valid: date
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
return {
|
|
40
|
+
invalid: true,
|
|
41
|
+
valid: null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function buildFieldValue({ data , usesTime , usesTimezone }) {
|
|
46
|
+
const date = datetimeFromUserInput(data);
|
|
47
|
+
if (date.invalid) {
|
|
48
|
+
return {
|
|
49
|
+
invalid: true
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
let format;
|
|
53
|
+
if (usesTimezone) {
|
|
54
|
+
format = 'YYYY-MM-DDTHH:mmZ';
|
|
55
|
+
} else if (usesTime) {
|
|
56
|
+
format = 'YYYY-MM-DDTHH:mm';
|
|
57
|
+
} else {
|
|
58
|
+
format = 'YYYY-MM-DD';
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
valid: date?.valid ? date.valid.format(format) : null,
|
|
62
|
+
invalid: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function getDefaultAMPM() {
|
|
66
|
+
return 'AM';
|
|
67
|
+
}
|
|
68
|
+
export function getDefaultUtcOffset() {
|
|
69
|
+
return startOfToday('Z');
|
|
70
|
+
}
|
|
71
|
+
export function userInputFromDatetime({ value , uses12hClock }) {
|
|
72
|
+
const datetime = fieldValueToMoment(value);
|
|
73
|
+
if (datetime) {
|
|
74
|
+
const timeFormat = uses12hClock ? 'hh:mm' : 'HH:mm';
|
|
75
|
+
return {
|
|
76
|
+
date: datetime,
|
|
77
|
+
time: datetime.format(timeFormat),
|
|
78
|
+
ampm: datetime.format('A'),
|
|
79
|
+
utcOffset: datetime.format('Z')
|
|
80
|
+
};
|
|
81
|
+
} else {
|
|
82
|
+
return {
|
|
83
|
+
ampm: getDefaultAMPM(),
|
|
84
|
+
utcOffset: getDefaultUtcOffset()
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const defaultZoneOffset = '+00:00';
|
|
2
|
+
export const zoneOffsets = [
|
|
3
|
+
'-12:00',
|
|
4
|
+
'-11:00',
|
|
5
|
+
'-10:00',
|
|
6
|
+
'-09:30',
|
|
7
|
+
'-09:00',
|
|
8
|
+
'-08:00',
|
|
9
|
+
'-07:00',
|
|
10
|
+
'-06:00',
|
|
11
|
+
'-05:00',
|
|
12
|
+
'-04:30',
|
|
13
|
+
'-04:00',
|
|
14
|
+
'-03:30',
|
|
15
|
+
'-03:00',
|
|
16
|
+
'-02:00',
|
|
17
|
+
'-01:00',
|
|
18
|
+
'+00:00',
|
|
19
|
+
'+01:00',
|
|
20
|
+
'+02:00',
|
|
21
|
+
'+03:00',
|
|
22
|
+
'+03:30',
|
|
23
|
+
'+04:00',
|
|
24
|
+
'+04:30',
|
|
25
|
+
'+05:00',
|
|
26
|
+
'+05:30',
|
|
27
|
+
'+05:45',
|
|
28
|
+
'+06:00',
|
|
29
|
+
'+06:30',
|
|
30
|
+
'+07:00',
|
|
31
|
+
'+08:00',
|
|
32
|
+
'+08:45',
|
|
33
|
+
'+09:00',
|
|
34
|
+
'+09:30',
|
|
35
|
+
'+10:00',
|
|
36
|
+
'+10:30',
|
|
37
|
+
'+11:00',
|
|
38
|
+
'+11:30',
|
|
39
|
+
'+12:00',
|
|
40
|
+
'+12:45',
|
|
41
|
+
'+13:00',
|
|
42
|
+
'+14:00'
|
|
43
|
+
];
|