dce-reactkit 3.5.4 → 3.5.6

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.
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Container that automatically scrolls when new items are added,
3
+ * lets the user scroll up to see old items, but resumes
4
+ * autoscroll when the user scrolls back to the bottom
5
+ * @author Gabe Abrams
6
+ */
7
+
8
+ // Import React
9
+ import React, { useReducer, useEffect, useRef } from 'react';
10
+
11
+ // Import FontAwesome
12
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13
+ import {
14
+ faChevronDown,
15
+ } from '@fortawesome/free-solid-svg-icons';
16
+
17
+ // Import shared types
18
+ import Variant from '../types/Variant';
19
+
20
+ // Import shared helper
21
+ import idify from '../helpers/idify';
22
+
23
+ /*------------------------------------------------------------------------*/
24
+ /* -------------------------------- Types ------------------------------- */
25
+ /*------------------------------------------------------------------------*/
26
+
27
+ // Props definition
28
+ type Props = {
29
+ // Name items in the container, "Messages" or "Comments" for example
30
+ // If excluded, a generic name will be used
31
+ itemsName?: string,
32
+ // Items in the scroll container
33
+ items: AutoScrollItem[],
34
+ // Custom variant for the "Jump to Bottom" button
35
+ jumpToBottomButtonVariant?: Variant,
36
+ };
37
+
38
+ // AutoScrollItem definition
39
+ type AutoScrollItem = {
40
+ // Unique item id
41
+ id: string | number,
42
+ // The item to render
43
+ item: React.ReactNode,
44
+ };
45
+
46
+ /*------------------------------------------------------------------------*/
47
+ /* -------------------------------- Style ------------------------------- */
48
+ /*------------------------------------------------------------------------*/
49
+
50
+ const style = `
51
+ .ScrollLockToBottom-outer-container {
52
+ /* Take up all space */
53
+ height: 100%;
54
+ position: relative;
55
+ }
56
+
57
+ .ScrollLockToBottom-scrollable-container {
58
+ /* Take up max 100% height, don't take it up if not needed */
59
+ /* (so column-reverse layout doesn't start at the bottom) */
60
+ max-height: 100%;
61
+ overflow-y: auto;
62
+
63
+ /* Allow children to position */
64
+ position: relative;
65
+
66
+ /* Reverse order of children so scroll starts at bottom */
67
+ /* (but children will have to be rendered in reverse order) */
68
+ display: flex;
69
+ flex-direction: column-reverse;
70
+ }
71
+
72
+ .ScrollLockToBottom-jump-to-bottom-container {
73
+ /* Don't take any space in parent */
74
+ height: 0;
75
+ overflow: visible;
76
+
77
+ /* On top of items */
78
+ z-index: 2;
79
+
80
+ /* Position in center bottom */
81
+ position: absolute;
82
+ bottom: 2rem;
83
+
84
+ /* Center horizontally */
85
+ width: 100%;
86
+ text-align: center;
87
+ }
88
+
89
+ .ScrollLockToBottom-item-container {
90
+ /* Normal Height */
91
+ position: relative;
92
+ z-index: 1;
93
+ }
94
+ `;
95
+
96
+ /*------------------------------------------------------------------------*/
97
+ /* ------------------------------ Constants ----------------------------- */
98
+ /*------------------------------------------------------------------------*/
99
+
100
+ // Scrolled to bottom threshold
101
+ // (how close you have to be to the bottom for it to count as the bottom)
102
+ const SCROLLED_TO_BOTTOM_THRESHOLD_REMS = 2;
103
+
104
+ /*------------------------------------------------------------------------*/
105
+ /* -------------------------- Static Functions -------------------------- */
106
+ /*------------------------------------------------------------------------*/
107
+
108
+ /**
109
+ * Get ids of items
110
+ * @author Gabe Abrams
111
+ * @param items the items in the container
112
+ * @returns ids of items
113
+ */
114
+ const getItemIds = (items: AutoScrollItem[]): (string | number)[] => {
115
+ return Array.from(items).map((child) => {
116
+ return child.id;
117
+ });
118
+ };
119
+
120
+ /*------------------------------------------------------------------------*/
121
+ /* -------------------------------- State ------------------------------- */
122
+ /*------------------------------------------------------------------------*/
123
+
124
+ /* -------- State Definition -------- */
125
+
126
+ type State = {
127
+ // If true, the user was scrolled to the bottom after last update
128
+ wasScrolledToBottom: boolean,
129
+ // If true, the "jump to bottom" button is visible
130
+ jumpToBottomButtonVisible: boolean,
131
+ };
132
+
133
+ /* ------------- Actions ------------ */
134
+
135
+ // Types of actions
136
+ enum ActionType {
137
+ // Identify that the user is now scrolled to the bottom
138
+ NowScrolledToBottom = 'NowScrolledToBottom',
139
+ // Identify that the user scrolled away from the bottom
140
+ NowScrolledAwayFromBottom = 'NowScrolledAwayFromBottom',
141
+ // Identify that new content appeared at the bottom but is not visible
142
+ NewContentAtBottom = 'NewContentAtBottom',
143
+ }
144
+
145
+ // Action definitions
146
+ type Action = (
147
+ | {
148
+ // Action type
149
+ type: (
150
+ | ActionType.NowScrolledToBottom
151
+ | ActionType.NowScrolledAwayFromBottom
152
+ | ActionType.NewContentAtBottom
153
+ ),
154
+ }
155
+ );
156
+
157
+ /**
158
+ * Reducer that executes actions
159
+ * @author Gabe Abrams
160
+ * @param state current state
161
+ * @param action action to execute
162
+ */
163
+ const reducer = (state: State, action: Action): State => {
164
+ switch (action.type) {
165
+ case ActionType.NowScrolledToBottom: {
166
+ return {
167
+ ...state,
168
+ // Store the event
169
+ wasScrolledToBottom: true,
170
+ // Hide the "jump to bottom" button
171
+ jumpToBottomButtonVisible: false,
172
+ };
173
+ }
174
+ case ActionType.NowScrolledAwayFromBottom: {
175
+ return {
176
+ ...state,
177
+ // Store the event
178
+ wasScrolledToBottom: false,
179
+ };
180
+ }
181
+ case ActionType.NewContentAtBottom: {
182
+ return {
183
+ ...state,
184
+ // Store the event
185
+ wasScrolledToBottom: false,
186
+ // Show the "jump to bottom" button
187
+ jumpToBottomButtonVisible: true,
188
+ };
189
+ }
190
+ default: {
191
+ return state;
192
+ }
193
+ }
194
+ };
195
+
196
+ /*------------------------------------------------------------------------*/
197
+ /* ------------------------------ Component ----------------------------- */
198
+ /*------------------------------------------------------------------------*/
199
+
200
+ const ScrollLockToBottom: React.FC<Props> = (props) => {
201
+ /*------------------------------------------------------------------------*/
202
+ /* -------------------------------- Setup ------------------------------- */
203
+ /*------------------------------------------------------------------------*/
204
+
205
+ /* -------------- Props ------------- */
206
+
207
+ // Destructure all props
208
+ const {
209
+ itemsName,
210
+ items,
211
+ jumpToBottomButtonVariant = Variant.Danger,
212
+ } = props;
213
+
214
+ /* -------------- State ------------- */
215
+
216
+ // Initial state
217
+ const initialState: State = {
218
+ wasScrolledToBottom: true,
219
+ jumpToBottomButtonVisible: false,
220
+ };
221
+
222
+ // Initialize state
223
+ const [state, dispatch] = useReducer(reducer, initialState);
224
+
225
+ // Destructure common state
226
+ const {
227
+ wasScrolledToBottom,
228
+ jumpToBottomButtonVisible,
229
+ } = state;
230
+
231
+ /* -------------- Refs -------------- */
232
+
233
+ // Initialize refs
234
+ const lastItemIds = useRef<(string | number)[]>([]);
235
+ const container = useRef<HTMLDivElement | null>(null);
236
+
237
+ /*------------------------------------------------------------------------*/
238
+ /* ------------------------- Component Functions ------------------------ */
239
+ /*------------------------------------------------------------------------*/
240
+
241
+ /*----------------------------------------*/
242
+ /* --------------- Scroll --------------- */
243
+ /*----------------------------------------*/
244
+
245
+ /**
246
+ * Check if the user is scrolled to the bottom
247
+ * @author Gabe Abrams
248
+ * @returns true if scrolled to the bottom
249
+ */
250
+ const isScrolledToBottom = (): boolean => {
251
+ // Skip if no container
252
+ if (!container.current) {
253
+ return true;
254
+ }
255
+
256
+ // Get info about container
257
+ const {
258
+ scrollTop,
259
+ } = container.current;
260
+
261
+ // Distance to bottom
262
+ const rootFontSizePx = Number.parseInt(
263
+ getComputedStyle(document.documentElement).fontSize,
264
+ 10,
265
+ );
266
+ const distanceToBottomRems = Math.abs(scrollTop / rootFontSizePx);
267
+
268
+ // Figure out if we're scrolled to the bottom
269
+ return (distanceToBottomRems < SCROLLED_TO_BOTTOM_THRESHOLD_REMS);
270
+ };
271
+
272
+ /**
273
+ * Scroll the user to the bottom of the container
274
+ * @author Gabe Abrams
275
+ */
276
+ const scrollToBottom = () => {
277
+ // Skip if no container
278
+ if (!container.current) {
279
+ return;
280
+ }
281
+
282
+ // Update state
283
+ dispatch({
284
+ type: ActionType.NowScrolledToBottom,
285
+ });
286
+
287
+ // Scroll to bottom
288
+ container.current.scrollTop = 0;
289
+ };
290
+
291
+ /*----------------------------------------*/
292
+ /* -------------- Handlers -------------- */
293
+ /*----------------------------------------*/
294
+
295
+ /**
296
+ * Handle scroll events on the container
297
+ * @author Gabe Abrams
298
+ */
299
+ const handleScroll = () => {
300
+ // Skip if no container
301
+ if (!container.current) {
302
+ return;
303
+ }
304
+
305
+ // Check if now scrolled to bottom
306
+ const nowScrolledToBottom = isScrolledToBottom();
307
+
308
+ // Remember if now no longer scrolled to bottom
309
+ if (nowScrolledToBottom) {
310
+ dispatch({
311
+ type: ActionType.NowScrolledToBottom,
312
+ });
313
+ } else {
314
+ dispatch({
315
+ type: ActionType.NowScrolledAwayFromBottom,
316
+ });
317
+ }
318
+ };
319
+
320
+ /*------------------------------------------------------------------------*/
321
+ /* ------------------------- Lifecycle Functions ------------------------ */
322
+ /*------------------------------------------------------------------------*/
323
+
324
+ /**
325
+ * Mount
326
+ * @author Gabe Abrams
327
+ */
328
+ useEffect(
329
+ () => {
330
+ // Scroll to the bottom
331
+ scrollToBottom();
332
+ },
333
+ [],
334
+ );
335
+
336
+ /**
337
+ * Update (also called on mount)
338
+ * @author Gabe Abrams
339
+ */
340
+ useEffect(
341
+ () => {
342
+ // Check if new content appeared
343
+ const currentItemIds = getItemIds(items);
344
+
345
+ // Check if new content appeared at bottom
346
+ const newContentAtBottom = (
347
+ currentItemIds.length > 0
348
+ && (
349
+ currentItemIds[currentItemIds.length - 1]
350
+ !== lastItemIds.current[lastItemIds.current.length - 1]
351
+ )
352
+ );
353
+
354
+ // Do nothing if no new content
355
+ if (!newContentAtBottom) {
356
+ return;
357
+ }
358
+
359
+ // Check if used to be scrolled to the bottom
360
+ if (wasScrolledToBottom) {
361
+ // Was scrolled to the bottom! Autoscroll.
362
+ scrollToBottom();
363
+ } else {
364
+ // Not scrolled to bottom. Show "jump to bottom" button.
365
+ dispatch({
366
+ type: ActionType.NewContentAtBottom,
367
+ });
368
+ }
369
+
370
+ // Update last item ids
371
+ lastItemIds.current = currentItemIds;
372
+ },
373
+ [items],
374
+ );
375
+
376
+ /*------------------------------------------------------------------------*/
377
+ /* ------------------------------- Render ------------------------------- */
378
+ /*------------------------------------------------------------------------*/
379
+
380
+ /*----------------------------------------*/
381
+ /* --------------- Main UI -------------- */
382
+ /*----------------------------------------*/
383
+
384
+ // Jump to Bottom button
385
+ let jumpToBottomButton: React.ReactNode;
386
+ if (jumpToBottomButtonVisible) {
387
+ jumpToBottomButton = (
388
+ <div className={`ScrollLockToBottom-jump-to-bottom-container ScrollLockToBottom-for-${idify(itemsName ?? 'items')}`}>
389
+ <button
390
+ type="button"
391
+ className={`ScrollLockToBottom-jump-to-bottom-button ScrollLockToBottom-jump-to-bottom-button-for-${idify(itemsName ?? 'items')} btn btn-sm btn-${jumpToBottomButtonVariant} pt-0 pb-0`}
392
+ onClick={scrollToBottom}
393
+ aria-label="scroll back to bottom and show new content"
394
+ >
395
+ New
396
+ {' '}
397
+ {itemsName ?? 'Content'}
398
+ <FontAwesomeIcon
399
+ icon={faChevronDown}
400
+ className="ms-1"
401
+ />
402
+ </button>
403
+ </div>
404
+ );
405
+ }
406
+
407
+ // Main UI
408
+ return (
409
+ <div className="ScrollLockToBottom-outer-container bg-warning">
410
+ {/* Style */}
411
+ <style>
412
+ {style}
413
+ </style>
414
+
415
+ {/* Jump to bottom button */}
416
+ {jumpToBottomButton}
417
+
418
+ {/* Scrollable Item Container */}
419
+ <div
420
+ className="ScrollLockToBottom-scrollable-container"
421
+ onScroll={() => {
422
+ handleScroll();
423
+ }}
424
+ ref={container}
425
+ >
426
+ {/* Items */}
427
+ {
428
+ items
429
+ // Render each item with a key
430
+ .map((item) => {
431
+ return (
432
+ <div
433
+ className="ScrollLockToBottom-item-container"
434
+ key={item.id}
435
+ >
436
+ {item.item}
437
+ </div>
438
+ );
439
+ })
440
+ // Reverse order because flex column is reverse
441
+ .reverse()
442
+ }
443
+ </div>
444
+ </div>
445
+ );
446
+ };
447
+
448
+ /*------------------------------------------------------------------------*/
449
+ /* ------------------------------- Wrap Up ------------------------------ */
450
+ /*------------------------------------------------------------------------*/
451
+
452
+ // Export component
453
+ export default ScrollLockToBottom;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A toggle switch that toggles on or off
3
+ * @author Alessandra De Lucas
4
+ * @author Gabe Abrams
5
+ */
6
+
7
+ // Import React
8
+ import React from 'react';
9
+
10
+ // Import FontAwesome
11
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
+ import { faCircle } from '@fortawesome/free-solid-svg-icons';
13
+
14
+ // Import shared types
15
+ import Variant from '../types/Variant';
16
+
17
+ /*------------------------------------------------------------------------*/
18
+ /* -------------------------------- Types ------------------------------- */
19
+ /*------------------------------------------------------------------------*/
20
+
21
+ // Props definition
22
+ type Props = {
23
+ // If true, the toggle switch is in the on position
24
+ isOn: boolean,
25
+ /**
26
+ * A handler to call when the switch is toggled
27
+ * @param isOn Updated value for isOn
28
+ */
29
+ onToggle: (isOn: boolean) => void,
30
+ // Unique ID for the toggle switch
31
+ id?: string,
32
+ // A description for what it means when the toggle switch is on
33
+ // E.g. "airplane mode is active"
34
+ // Description will be placed into a sentence with the structure:
35
+ // `If on, ${description}. Currently on. Click to turn off.`
36
+ description: string,
37
+ // Custom background variant for the "on" state (defaults to Variant.Info)
38
+ backgroundVariantWhenOn?: Variant,
39
+ };
40
+
41
+ /*------------------------------------------------------------------------*/
42
+ /* ------------------------------ Component ----------------------------- */
43
+ /*------------------------------------------------------------------------*/
44
+
45
+ const ToggleSwitch: React.FC<Props> = (props) => {
46
+ /*------------------------------------------------------------------------*/
47
+ /* -------------------------------- Setup ------------------------------- */
48
+ /*------------------------------------------------------------------------*/
49
+
50
+ /* -------------- Props ------------- */
51
+
52
+ // Destructure all props
53
+ const {
54
+ isOn,
55
+ onToggle,
56
+ id,
57
+ description,
58
+ backgroundVariantWhenOn = Variant.Info,
59
+ } = props;
60
+
61
+ /*------------------------------------------------------------------------*/
62
+ /* ------------------------------- Render ------------------------------- */
63
+ /*------------------------------------------------------------------------*/
64
+
65
+ /*----------------------------------------*/
66
+ /* --------------- Main UI -------------- */
67
+ /*----------------------------------------*/
68
+
69
+ const backgroundVariant = (
70
+ isOn
71
+ ? backgroundVariantWhenOn
72
+ : 'secondary'
73
+ );
74
+
75
+ return (
76
+ <button
77
+ id={id}
78
+ aria-label={`If on, ${description}. Currently ${isOn ? 'on' : 'off'}. Click to turn ${isOn ? 'off' : 'on'}.`}
79
+ className={`alert alert-${backgroundVariant} bg-${backgroundVariant} mb-0 rounded-pill d-inline-block pt-0 pb-0 px-3`}
80
+ onClick={() => {
81
+ onToggle(!isOn);
82
+ }}
83
+ type="button"
84
+ >
85
+ <FontAwesomeIcon
86
+ icon={faCircle}
87
+ className="text-light"
88
+ style={{
89
+ transform: `translateX(${isOn ? '' : '-'}0.7rem)`,
90
+ transition: '0.3s transform ease-in-out',
91
+ }}
92
+ />
93
+ </button>
94
+ );
95
+ };
96
+
97
+ /*------------------------------------------------------------------------*/
98
+ /* ------------------------------- Wrap Up ------------------------------ */
99
+ /*------------------------------------------------------------------------*/
100
+
101
+ // Export component
102
+ export default ToggleSwitch;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Convert a string to hyphenated lowercase format with no space or
3
+ * non-alphanumeric characters
4
+ * @author Gabe Abrams
5
+ * @param str the string to convert
6
+ * @returns the idified string
7
+ */
8
+ const idify = (str: string): string => {
9
+ return (
10
+ str
11
+ // Trim whitespace
12
+ .trim()
13
+ // Convert to lowercase
14
+ .toLowerCase()
15
+ // Replace non-alphanumeric characters with hyphens
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ // Change multiple hyphens in a row for a single hyphen
18
+ .replace(/-+/g, '-')
19
+ // Remove hyphens at the beginning and end of the string
20
+ .replace(/^-+|-+$/g, '')
21
+ );
22
+ };
23
+
24
+ export default idify;
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  // Import components
2
- import AppWrapper, { alert, confirm, showFatalError } from './components/AppWrapper';
2
+ import AppWrapper, {
3
+ alert,
4
+ confirm,
5
+ showFatalError,
6
+ addFatalErrorHandler,
7
+ } from './components/AppWrapper';
3
8
  import LoadingSpinner from './components/LoadingSpinner';
4
9
  import ErrorBox from './components/ErrorBox';
5
10
  import Modal from './components/Modal';
@@ -19,6 +24,8 @@ import IntelliTable from './components/IntelliTable';
19
24
  import CSVDownloadButton from './components/CSVDownloadButton';
20
25
  import DBEntryManagerPanel from './components/DBEntryManagerPanel';
21
26
  import Tooltip from './components/Tooltip';
27
+ import ToggleSwitch from './components/ToggleSwitch';
28
+ import ScrollLockToBottom from './components/ScrollLockToBottomContainer';
22
29
 
23
30
  // Import errors
24
31
  import ErrorWithCode from './errors/ErrorWithCode';
@@ -71,6 +78,7 @@ import genCommaList from './helpers/genCommaList';
71
78
  import validateEmail from './helpers/validators/validateEmail';
72
79
  import validatePhoneNumber from './helpers/validators/validatePhoneNumber';
73
80
  import validateString from './helpers/validators/validateString';
81
+ import idify from './helpers/idify';
74
82
 
75
83
  // Import types
76
84
  import ModalButtonType from './types/ModalButtonType';
@@ -117,6 +125,8 @@ export {
117
125
  CSVDownloadButton,
118
126
  DBEntryManagerPanel,
119
127
  Tooltip,
128
+ ToggleSwitch,
129
+ ScrollLockToBottom,
120
130
  // Global functions
121
131
  alert,
122
132
  confirm,
@@ -160,10 +170,12 @@ export {
160
170
  validatePhoneNumber,
161
171
  validateString,
162
172
  getLocalTimeInfo,
173
+ idify,
163
174
  // Client helpers
164
175
  initClient,
165
176
  visitServerEndpoint,
166
177
  logClientEvent,
178
+ addFatalErrorHandler,
167
179
  // Server helpers
168
180
  initServer,
169
181
  genRouteHandler,