@performant-software/shared-components 0.5.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.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/build/index.js +2 -0
- package/build/index.js.map +1 -0
- package/build/main.css +11 -0
- package/index.js +1 -0
- package/package.json +36 -0
- package/src/api/Attachments.js +28 -0
- package/src/api/BaseService.js +127 -0
- package/src/api/BaseTransform.js +55 -0
- package/src/api/FormDataTransform.js +30 -0
- package/src/api/NestedAttributesTransform.js +63 -0
- package/src/components/EditContainer.css +0 -0
- package/src/components/EditContainer.js +448 -0
- package/src/components/GoogleAnalytics.css +0 -0
- package/src/components/GoogleAnalytics.js +118 -0
- package/src/components/GoogleScript.js +5 -0
- package/src/components/InfiniteScroll.css +1 -0
- package/src/components/InfiniteScroll.js +120 -0
- package/src/components/Keyboard.css +11 -0
- package/src/components/Keyboard.js +55 -0
- package/src/i18n/en.json +204 -0
- package/src/i18n/i18n.js +24 -0
- package/src/index.js +34 -0
- package/src/utils/Browser.js +8 -0
- package/src/utils/Calendar.js +232 -0
- package/src/utils/Date.js +10 -0
- package/src/utils/DragDrop.js +17 -0
- package/src/utils/Element.js +36 -0
- package/src/utils/Map.js +27 -0
- package/src/utils/Object.js +114 -0
- package/src/utils/String.js +20 -0
- package/src/utils/Timer.js +32 -0
- package/src/utils/Utility.js +14 -0
- package/test/api/Attachments.spec.js +32 -0
- package/types/api/Attachments.js.flow +28 -0
- package/types/api/BaseService.js.flow +127 -0
- package/types/api/BaseTransform.js.flow +55 -0
- package/types/api/FormDataTransform.js.flow +30 -0
- package/types/api/NestedAttributesTransform.js.flow +63 -0
- package/types/components/EditContainer.js.flow +448 -0
- package/types/components/GoogleAnalytics.js.flow +118 -0
- package/types/components/GoogleScript.js.flow +5 -0
- package/types/components/InfiniteScroll.js.flow +120 -0
- package/types/components/Keyboard.js.flow +55 -0
- package/types/i18n/i18n.js.flow +24 -0
- package/types/index.js.flow +34 -0
- package/types/utils/Browser.js.flow +8 -0
- package/types/utils/Calendar.js.flow +232 -0
- package/types/utils/Date.js.flow +10 -0
- package/types/utils/DragDrop.js.flow +17 -0
- package/types/utils/Element.js.flow +36 -0
- package/types/utils/Map.js.flow +27 -0
- package/types/utils/Object.js.flow +114 -0
- package/types/utils/String.js.flow +20 -0
- package/types/utils/Timer.js.flow +32 -0
- package/types/utils/Utility.js.flow +14 -0
- package/webpack.config.js +3 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
|
+
import Keyboard from 'simple-keyboard';
|
|
5
|
+
import './Keyboard.css';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
layout: any,
|
|
9
|
+
onChange: (value: string) => void,
|
|
10
|
+
value: string,
|
|
11
|
+
keyboardClass: string,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const KEY_CAPS_LOCK = '{lock}';
|
|
15
|
+
const KEY_SHIFT = '{shift}';
|
|
16
|
+
|
|
17
|
+
const LAYOUT_DEFAULT = 'default';
|
|
18
|
+
const LAYOUT_SHIFT = 'shift';
|
|
19
|
+
|
|
20
|
+
const KeyboardSimple = (props: Props) => {
|
|
21
|
+
const keyboardRef = useRef();
|
|
22
|
+
|
|
23
|
+
// Toggles the layout name for Shift and CapsLock keys.
|
|
24
|
+
const onKeyPress = (value) => {
|
|
25
|
+
const isCaps = value === KEY_CAPS_LOCK || value === KEY_SHIFT;
|
|
26
|
+
|
|
27
|
+
if (isCaps && keyboardRef.current && keyboardRef.current.options.layoutName === LAYOUT_SHIFT) {
|
|
28
|
+
keyboardRef.current.setOptions({ layoutName: LAYOUT_DEFAULT });
|
|
29
|
+
} else if (isCaps && keyboardRef.current && keyboardRef.current.options.layoutName !== LAYOUT_SHIFT) {
|
|
30
|
+
keyboardRef.current.setOptions({ layoutName: LAYOUT_SHIFT });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Sets up the keyboard reference and the initial value when the component is first rendered.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const { layout, onChange } = props;
|
|
37
|
+
keyboardRef.current = new Keyboard(`.${props.keyboardClass}`, { ...layout, onChange, onKeyPress });
|
|
38
|
+
keyboardRef.current.setInput(props.value);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// Sets the keyboard value when the value property changes.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (keyboardRef.current) {
|
|
44
|
+
keyboardRef.current.setInput(props.value);
|
|
45
|
+
}
|
|
46
|
+
}, [props.value]);
|
|
47
|
+
|
|
48
|
+
return <div className={props.keyboardClass} />;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
KeyboardSimple.defaultProps = {
|
|
52
|
+
keyboardClass: 'simple-keyboard'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default KeyboardSimple;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
|
|
3
|
+
import en from './en.json';
|
|
4
|
+
|
|
5
|
+
const resources = {
|
|
6
|
+
en: {
|
|
7
|
+
translation: en
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const i18n = i18next.createInstance();
|
|
12
|
+
|
|
13
|
+
i18n
|
|
14
|
+
.init({
|
|
15
|
+
debug: true,
|
|
16
|
+
fallbackLng: 'en',
|
|
17
|
+
lng: 'en',
|
|
18
|
+
interpolation: {
|
|
19
|
+
escapeValue: false,
|
|
20
|
+
},
|
|
21
|
+
resources
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default i18n;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
// API
|
|
4
|
+
export { default as Attachments } from './api/Attachments';
|
|
5
|
+
export { default as BaseService } from './api/BaseService';
|
|
6
|
+
export { default as BaseTransform } from './api/BaseTransform';
|
|
7
|
+
export { default as FormDataTransform } from './api/FormDataTransform';
|
|
8
|
+
export { default as NestedAttributesTransform } from './api/NestedAttributesTransform';
|
|
9
|
+
|
|
10
|
+
// Components
|
|
11
|
+
export { default as useEditContainer } from './components/EditContainer';
|
|
12
|
+
export { default as withGoogleAnalytics } from './components/GoogleAnalytics';
|
|
13
|
+
export { default as GoogleScript } from './components/GoogleScript';
|
|
14
|
+
export { default as InfiniteScroll } from './components/InfiniteScroll';
|
|
15
|
+
export { default as Keyboard } from './components/Keyboard';
|
|
16
|
+
|
|
17
|
+
// I18n
|
|
18
|
+
export { default as i18n } from './i18n/i18n';
|
|
19
|
+
|
|
20
|
+
// Utils
|
|
21
|
+
export * as Browser from './utils/Browser';
|
|
22
|
+
export { default as Calendar } from './utils/Calendar';
|
|
23
|
+
export { default as Date } from './utils/Date';
|
|
24
|
+
export { default as useDragDrop } from './utils/DragDrop';
|
|
25
|
+
export { default as Element } from './utils/Element';
|
|
26
|
+
export { default as Map } from './utils/Map';
|
|
27
|
+
export { default as Object } from './utils/Object';
|
|
28
|
+
export { default as String } from './utils/String';
|
|
29
|
+
export { default as Timer } from './utils/Timer';
|
|
30
|
+
export { default as Utility } from './utils/Utility'; // TODO: Rename me
|
|
31
|
+
|
|
32
|
+
// Types
|
|
33
|
+
export type { EditContainerProps as EditModalProps } from './components/EditContainer'; // Backwards compatability
|
|
34
|
+
export type { EditContainerProps } from './components/EditContainer';
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import moment from 'moment-islamic-civil';
|
|
4
|
+
import 'moment/min/locales';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LOCALE = 'en';
|
|
7
|
+
|
|
8
|
+
const MAX_DAYS_IN_MONTH = 31;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wrapper class to handle momentJS dates to multiple calendars.
|
|
12
|
+
*/
|
|
13
|
+
class Calendar {
|
|
14
|
+
static Calendars: any;
|
|
15
|
+
static DateFormats: any;
|
|
16
|
+
static Defaults: any;
|
|
17
|
+
|
|
18
|
+
locale: string;
|
|
19
|
+
name: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Constructs a new calendar instance for the passed locale and calendar.
|
|
23
|
+
*
|
|
24
|
+
* @param locale
|
|
25
|
+
* @param name
|
|
26
|
+
*/
|
|
27
|
+
constructor(locale: string = DEFAULT_LOCALE, name: string = Calendar.Calendars.gregorian) {
|
|
28
|
+
this.locale = locale;
|
|
29
|
+
this.name = name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Increments the date by the passed value.
|
|
34
|
+
*
|
|
35
|
+
* @param date
|
|
36
|
+
* @param value
|
|
37
|
+
*
|
|
38
|
+
* @returns {*}
|
|
39
|
+
*/
|
|
40
|
+
addDate(date: Date, value: number) {
|
|
41
|
+
const m = this.moment(date);
|
|
42
|
+
return this.isHijri() ? m.add(value, 'iDate') : m.add(value, 'date');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Increments the month by the passed value.
|
|
47
|
+
*
|
|
48
|
+
* @param date
|
|
49
|
+
* @param value
|
|
50
|
+
*
|
|
51
|
+
* @returns {*}
|
|
52
|
+
*/
|
|
53
|
+
addMonth(date: Date, value: number) {
|
|
54
|
+
const m = this.moment(date);
|
|
55
|
+
return this.isHijri() ? m.add(value, 'iMonth') : m.add(value, 'month');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Increments the year by the passed value.
|
|
60
|
+
*
|
|
61
|
+
* @param date
|
|
62
|
+
* @param value
|
|
63
|
+
*
|
|
64
|
+
* @returns {*}
|
|
65
|
+
*/
|
|
66
|
+
addYear(date: Date, value: number) {
|
|
67
|
+
const m = this.moment(date);
|
|
68
|
+
return this.isHijri() ? m.add(value, 'iYear') : m.add(value, 'year');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Converts the passed year, month, and date to a moment Date object.
|
|
73
|
+
*
|
|
74
|
+
* @param year
|
|
75
|
+
* @param month
|
|
76
|
+
* @param date
|
|
77
|
+
*
|
|
78
|
+
* @returns {moment.Moment}
|
|
79
|
+
*/
|
|
80
|
+
convertToDate({ year, month, date }: { year: number, month: number, date: number }) {
|
|
81
|
+
let m = this.moment().hours(0).minutes(0).seconds(0);
|
|
82
|
+
|
|
83
|
+
if (this.isHijri()) {
|
|
84
|
+
m = m
|
|
85
|
+
.iYear(year || this.getDefaultYear())
|
|
86
|
+
.iMonth(month || this.getDefaultMonth())
|
|
87
|
+
.iDate(date || this.getDefaultDate());
|
|
88
|
+
} else {
|
|
89
|
+
m = m
|
|
90
|
+
.year(year || this.getDefaultYear())
|
|
91
|
+
.month(month || this.getDefaultMonth())
|
|
92
|
+
.date(date || this.getDefaultDate());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return m;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the days in the month for the passed year/month.
|
|
100
|
+
*
|
|
101
|
+
* @param year
|
|
102
|
+
* @param month
|
|
103
|
+
*
|
|
104
|
+
* @returns {number}
|
|
105
|
+
*/
|
|
106
|
+
daysInMonth(year: number, month: number) {
|
|
107
|
+
const y = year || this.getDefaultYear();
|
|
108
|
+
const m = month || this.getDefaultMonth();
|
|
109
|
+
|
|
110
|
+
const date = this.convertToDate({ year: y, month: m, date: this.getDefaultDate() });
|
|
111
|
+
return (this.isHijri() ? date.iDaysInMonth() : date.daysInMonth()) || MAX_DAYS_IN_MONTH;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Formats the passed date.
|
|
116
|
+
*
|
|
117
|
+
* @param date
|
|
118
|
+
* @param format
|
|
119
|
+
*
|
|
120
|
+
* @returns {*}
|
|
121
|
+
*/
|
|
122
|
+
format(date: moment, format: number = Calendar.DateFormats.gregorian) {
|
|
123
|
+
return this.moment(date).format(Calendar.DateFormats[this.name][format]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns the default date for the current calendar.
|
|
128
|
+
*
|
|
129
|
+
* @returns {*}
|
|
130
|
+
*/
|
|
131
|
+
getDefaultDate() {
|
|
132
|
+
return Calendar.Defaults[this.name].date;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns the default month for the current calendar.
|
|
137
|
+
*
|
|
138
|
+
* @returns {*}
|
|
139
|
+
*/
|
|
140
|
+
getDefaultMonth() {
|
|
141
|
+
return Calendar.Defaults[this.name].month;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns the default year for the current calendar.
|
|
146
|
+
*
|
|
147
|
+
* @returns {number|*|"numeric"|"2-digit"|string|((y: number) => moment.Moment)|(() => number)|moment.numberlike}
|
|
148
|
+
*/
|
|
149
|
+
getDefaultYear() {
|
|
150
|
+
return Calendar.Defaults[this.name].year;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Returns true if the current calendar is Hijri.
|
|
155
|
+
*
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
isHijri() {
|
|
159
|
+
return this.name === Calendar.Calendars.hijri;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns the list of months for the current calendar.
|
|
164
|
+
*
|
|
165
|
+
* @returns {*}
|
|
166
|
+
*/
|
|
167
|
+
listMonths() {
|
|
168
|
+
const localeData = this.moment().localeData();
|
|
169
|
+
return this.isHijri() ? localeData._iMonths : localeData.months();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Wraps the passed date/string in a moment object.
|
|
174
|
+
*
|
|
175
|
+
* @param date
|
|
176
|
+
*/
|
|
177
|
+
moment(date: ?any = null) {
|
|
178
|
+
const m = date ? moment(date) : moment();
|
|
179
|
+
m.locale(this.locale);
|
|
180
|
+
return m;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parses the passed date into year, month, and date components.
|
|
185
|
+
*
|
|
186
|
+
* @param date
|
|
187
|
+
*
|
|
188
|
+
* @returns {{date: (*), month: (*), year: (*)}}
|
|
189
|
+
*/
|
|
190
|
+
parseDate(date: Date | string) {
|
|
191
|
+
const m = this.moment(date);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
year: this.isHijri() ? m.iYear() : m.year(),
|
|
195
|
+
month: this.isHijri() ? m.iMonth() : m.month(),
|
|
196
|
+
date: this.isHijri() ? m.iDate() : m.date()
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Calendar.Calendars = {
|
|
202
|
+
gregorian: 'gregorian',
|
|
203
|
+
hijri: 'hijri'
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
Calendar.DateFormats = {
|
|
207
|
+
[Calendar.Calendars.gregorian]: {
|
|
208
|
+
'0': 'YYYY',
|
|
209
|
+
'1': 'MMMM YYYY',
|
|
210
|
+
'2': 'L'
|
|
211
|
+
},
|
|
212
|
+
[Calendar.Calendars.hijri]: {
|
|
213
|
+
'0': 'iYYYY',
|
|
214
|
+
'1': 'iMMMM iYYYY',
|
|
215
|
+
'2': 'iM/iD/iYYYY'
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
Calendar.Defaults = {
|
|
220
|
+
[Calendar.Calendars.gregorian]: {
|
|
221
|
+
year: 0,
|
|
222
|
+
month: 0,
|
|
223
|
+
date: 1
|
|
224
|
+
},
|
|
225
|
+
[Calendar.Calendars.hijri]: {
|
|
226
|
+
year: 1410,
|
|
227
|
+
month: 0,
|
|
228
|
+
date: 1
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export default Calendar;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, { type ComponentType } from 'react';
|
|
4
|
+
import { DndProvider } from 'react-dnd';
|
|
5
|
+
import { HTML5Backend, type BackendFactory } from 'react-dnd-html5-backend';
|
|
6
|
+
|
|
7
|
+
const useDragDrop = (WrappedComponent: ComponentType<any>, backend: BackendFactory = HTML5Backend) => (
|
|
8
|
+
() => (
|
|
9
|
+
<DndProvider
|
|
10
|
+
backend={backend}
|
|
11
|
+
>
|
|
12
|
+
<WrappedComponent />
|
|
13
|
+
</DndProvider>
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export default useDragDrop;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the subset of children matching the passed component type. This function is useful for designing components
|
|
7
|
+
* using a sub-component type syntax:
|
|
8
|
+
*
|
|
9
|
+
* <MyComponent ...>
|
|
10
|
+
* <MyComponent.Subcomponent ... />
|
|
11
|
+
* <MyComponent.AnotherSubcomponent ... />
|
|
12
|
+
* </MyComponent>
|
|
13
|
+
*
|
|
14
|
+
* @param children
|
|
15
|
+
* @param component
|
|
16
|
+
*
|
|
17
|
+
* @returns {[]}
|
|
18
|
+
*/
|
|
19
|
+
const findByType = (children: any, component: any) => {
|
|
20
|
+
const components = [];
|
|
21
|
+
|
|
22
|
+
const type = [component.displayName || component.name];
|
|
23
|
+
|
|
24
|
+
React.Children.forEach(children, (child) => {
|
|
25
|
+
const childType = child && child.type && (child.type.displayName || child.type.name);
|
|
26
|
+
if (type.includes(childType)) {
|
|
27
|
+
components.push(child);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return components;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
findByType
|
|
36
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the numeric latitude and longitude for the passed coordinates.
|
|
5
|
+
*
|
|
6
|
+
* @param coords
|
|
7
|
+
*
|
|
8
|
+
* @returns {*}
|
|
9
|
+
*/
|
|
10
|
+
const getPosition = (coords: ?{ lat: any, lng: any }) => {
|
|
11
|
+
let position;
|
|
12
|
+
|
|
13
|
+
if (coords) {
|
|
14
|
+
const lat = Number.parseFloat(coords.lat);
|
|
15
|
+
const lng = Number.parseFloat(coords.lng);
|
|
16
|
+
|
|
17
|
+
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
|
|
18
|
+
position = { lat, lng };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return position;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
getPosition
|
|
27
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import _ from 'underscore';
|
|
4
|
+
|
|
5
|
+
type OptionsProps = {
|
|
6
|
+
emptyValues: Array<any>,
|
|
7
|
+
ignoreHtml: boolean
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const EMPTY_VALUES = [
|
|
11
|
+
'',
|
|
12
|
+
null,
|
|
13
|
+
undefined,
|
|
14
|
+
[],
|
|
15
|
+
{}
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const DEFAULT_OPTIONS = {
|
|
19
|
+
emptyValues: EMPTY_VALUES,
|
|
20
|
+
ignoreHtml: true,
|
|
21
|
+
ignoreWhitespace: true
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const HTML_REGEX = /(<([^>]+)>)/gi;
|
|
25
|
+
const WHITESPACE_REGEX = /\s\s+/g;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true if the passed value is considered "empty".
|
|
29
|
+
*
|
|
30
|
+
* @param value
|
|
31
|
+
*
|
|
32
|
+
* @returns {boolean|*}
|
|
33
|
+
*/
|
|
34
|
+
export const isEmpty = (value: any) => {
|
|
35
|
+
// If the value is an object or array, use underscore's isEmpty check.
|
|
36
|
+
if (_.isObject(value) || _.isArray(value)) {
|
|
37
|
+
return _.isEmpty(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return !value;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns true if the passed two arguments as deep equal. This function will perform a recursive check against all of
|
|
45
|
+
* the keys in the objects.
|
|
46
|
+
*
|
|
47
|
+
* @param a
|
|
48
|
+
* @param b
|
|
49
|
+
*
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export const isEqual = (a: any, b: any, userOptions: OptionsProps = {}) => {
|
|
53
|
+
const options = _.defaults(userOptions, DEFAULT_OPTIONS);
|
|
54
|
+
|
|
55
|
+
// Check equality, consider empty strings and "null" values as equal
|
|
56
|
+
if (a === b || (_.contains(options.emptyValues, a) && _.contains(options.emptyValues, b))) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Deep string comparison
|
|
61
|
+
if (_.isString(a) && _.isString(b)) {
|
|
62
|
+
let aString = a;
|
|
63
|
+
let bString = b;
|
|
64
|
+
|
|
65
|
+
// Remove superfluous whitespace
|
|
66
|
+
if (options.ignoreWhitespace) {
|
|
67
|
+
aString = a.replace(WHITESPACE_REGEX, ' ');
|
|
68
|
+
bString = b.replace(WHITESPACE_REGEX, ' ');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If we're ignoring HTML, compare the string values with HTML tags removed
|
|
72
|
+
if (options.ignoreHtml) {
|
|
73
|
+
aString = aString.replace(HTML_REGEX, '');
|
|
74
|
+
bString = bString.replace(HTML_REGEX, '');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (aString === bString) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (a !== null && typeof a === 'object' && b !== null && typeof b === 'object') {
|
|
83
|
+
const aKeys = _.keys(a);
|
|
84
|
+
const bKeys = _.keys(b);
|
|
85
|
+
|
|
86
|
+
// If the objects contain different number of keys, return false
|
|
87
|
+
if (aKeys.length !== bKeys.length) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Recursively check each key for equality
|
|
92
|
+
let equal = true;
|
|
93
|
+
|
|
94
|
+
_.each(_.keys(a), (key) => {
|
|
95
|
+
if (!(_.has(b, key) && isEqual(a[key], b[key]))) {
|
|
96
|
+
equal = false;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// If any of the recursive keys are not equal, return false
|
|
101
|
+
if (!equal) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If we've made it this far, we've checked the equality of all the keys in both objects, return true
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
|
|
114
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import _ from 'underscore';
|
|
4
|
+
|
|
5
|
+
const includes = (a: any, b: any) => (
|
|
6
|
+
a && b && a.toString().toLowerCase().includes(b.toString().toLowerCase())
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const toString = (value: any) => {
|
|
10
|
+
if (_.isNumber(value) || _.isBoolean(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value || '';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
includes,
|
|
19
|
+
toString
|
|
20
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT = 500;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The timer class encapsulates the logic for setting and clearing a timeout. This is particularly useful for
|
|
7
|
+
* keydown/keyup events when we only want to perform an action after the user has finished typing.
|
|
8
|
+
*/
|
|
9
|
+
class Timer {
|
|
10
|
+
timeout: TimeoutID | null;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.timeout = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clears the search timer.
|
|
18
|
+
*/
|
|
19
|
+
clearSearchTimer() {
|
|
20
|
+
clearTimeout(this.timeout);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sets the search timer.
|
|
25
|
+
*/
|
|
26
|
+
setSearchTimer(onTimeout: () => void) {
|
|
27
|
+
clearTimeout(this.timeout);
|
|
28
|
+
this.timeout = setTimeout(onTimeout, DEFAULT_TIMEOUT);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default new Timer();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if passed value is a promise (i.e, has a .then method)
|
|
5
|
+
*
|
|
6
|
+
* @param *
|
|
7
|
+
*
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
const isPromise = (value: any) => !!value && typeof value === 'object' && typeof value.then === 'function';
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
isPromise
|
|
14
|
+
};
|