@dvrd/dvr-controls 1.0.57 → 1.0.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvrd/dvr-controls",
3
- "version": "1.0.57",
3
+ "version": "1.0.59",
4
4
  "description": "Custom web controls",
5
5
  "main": "index.ts",
6
6
  "files": [
@@ -4,19 +4,23 @@
4
4
  import './style/dvrdDatePicker.scss';
5
5
 
6
6
  import classNames from 'classnames';
7
- import React, {MouseEventHandler, useEffect, useMemo, useRef, useState} from 'react';
8
- import {ChangeFunction, ErrorType} from '../util/interfaces';
7
+ import React, {Fragment, MouseEventHandler, useEffect, useMemo, useRef, useState} from 'react';
8
+ import {ChangeFunction, DatePickerTimeMode, ErrorType} from '../util/interfaces';
9
9
  import AwesomeIcon from '../icon/awesomeIcon';
10
10
  import WithBackground from '../popup/withBackground';
11
11
  import DvrdButton from "../button/dvrdButton";
12
12
  import {generateComponentId} from "../util/componentUtil";
13
13
  import IDate from '@dvrd/idate';
14
+ import {pad, padNum, voidFunction} from "../util/controlUtil";
15
+ import {DvrdNumberInput} from "../../../index";
16
+ import defer from 'lodash.defer';
14
17
 
15
18
  interface Props {
16
19
  onChange: ChangeFunction<IDate>;
17
20
  closeOnChange?: boolean;
18
21
  value: IDate | null;
19
22
  label: string;
23
+ timeMode?: DatePickerTimeMode;
20
24
  error?: ErrorType;
21
25
  className?: string;
22
26
  placeholder?: string;
@@ -32,12 +36,20 @@ interface Props {
32
36
  export default function DvrdDatePicker(props: Props) {
33
37
  const {
34
38
  onChange, className, closeOnChange, error, label, value, placeholder, disabled, alwaysShowArrows,
35
- useMobileNative, max, min
39
+ useMobileNative, max, min, timeMode
36
40
  } = props;
37
41
  const [pickerOpen, setPickerOpen] = useState(false);
42
+ const [timePickerOpen, setTimePickerOpen] = useState(false);
38
43
  const dateFormat = useMemo(() => {
39
- return props.dateFormat ?? 'YYYY-MM-DD';
40
- }, [props.dateFormat]);
44
+ if (props.dateFormat) return props.dateFormat;
45
+ let format = 'YYYY-MM-DD';
46
+ if (timeMode === DatePickerTimeMode.FULL) format += ' HH:mm:ss';
47
+ else if (timeMode === DatePickerTimeMode.HOURS_MINUTES) format += ' HH:mm';
48
+ else if (timeMode === DatePickerTimeMode.HOURS_ONLY) format += ' HH';
49
+ else if (timeMode === DatePickerTimeMode.MINUTES_ONLY) format += ' mm';
50
+ else if (timeMode === DatePickerTimeMode.SECONDS_ONLY) format += ' ss';
51
+ return format;
52
+ }, [props.dateFormat, timeMode]);
41
53
  const id = useMemo(() => {
42
54
  return generateComponentId(props.id);
43
55
  }, [props.id]);
@@ -53,11 +65,19 @@ export default function DvrdDatePicker(props: Props) {
53
65
 
54
66
  const onChangePicker = (daySelected: boolean) => (value: IDate) => {
55
67
  onChange(value.clone());
56
- if (closeOnChange && daySelected) onClosePicker();
68
+ if (closeOnChange && daySelected) {
69
+ onClosePicker();
70
+ if (!!timeMode) setTimePickerOpen(true);
71
+ }
57
72
  };
58
73
 
74
+ function onChangeTimePicker(value: IDate) {
75
+ onChange(value.clone());
76
+ setTimePickerOpen(false);
77
+ }
78
+
59
79
  function onClosePicker() {
60
- setPickerOpen(false)
80
+ setPickerOpen(false);
61
81
  }
62
82
 
63
83
  function renderMobilePicker() {
@@ -80,14 +100,17 @@ export default function DvrdDatePicker(props: Props) {
80
100
  placeholder ?? ''}</label>
81
101
  <AwesomeIcon name='calendar-alt' className='calendar-icon'/>
82
102
  </div>
83
- <Picker onClose={onClosePicker} onChange={onChangePicker} open={pickerOpen} value={value}
84
- alwaysShowArrows={alwaysShowArrows} max={max} min={min}/>
103
+ <DatePicker onClose={onClosePicker} onChange={onChangePicker} open={pickerOpen} value={value}
104
+ alwaysShowArrows={alwaysShowArrows} max={max} min={min}/>
105
+ {!!timeMode &&
106
+ <TimePicker onChange={onChangeTimePicker} onClose={onClosePicker} value={value}
107
+ open={timePickerOpen} timeMode={timeMode}/>}
85
108
  <label className='error-label'>{error}</label>
86
109
  </div>
87
110
  )
88
111
  }
89
112
 
90
- interface PickerProps {
113
+ interface DatePickerProps {
91
114
  onChange: (daySelected: boolean) => ChangeFunction<IDate>;
92
115
  onClose: VoidFunction;
93
116
  value: IDate | null;
@@ -97,7 +120,7 @@ interface PickerProps {
97
120
  min?: IDate | string;
98
121
  }
99
122
 
100
- function Picker(props: PickerProps) {
123
+ function DatePicker(props: DatePickerProps) {
101
124
  const {min, max, alwaysShowArrows, open, onClose, value, onChange} = props;
102
125
  const divRef = useRef<HTMLDivElement>(null);
103
126
  const pickerValue = useMemo(() => new IDate(value), [value]);
@@ -401,4 +424,141 @@ function Picker(props: PickerProps) {
401
424
  </div>
402
425
  </WithBackground>
403
426
  )
427
+ }
428
+
429
+ interface TimePickerProps {
430
+ onChange: ChangeFunction<IDate>;
431
+ onClose: VoidFunction;
432
+ value: IDate | null;
433
+ open: boolean;
434
+ timeMode: DatePickerTimeMode;
435
+ }
436
+
437
+ type TimePartValues = [string, string, string, string, string, string];
438
+
439
+ function dateToTimeParts(value: IDate | null): TimePartValues {
440
+ if (!value) return ['', '', '', '', '', ''];
441
+ const hours = padNum(value.hours());
442
+ const minutes = padNum(value.minutes());
443
+ const seconds = padNum(value.seconds());
444
+ return [...hours.split(''), ...minutes.split(''), ...seconds.split('')] as TimePartValues;
445
+ }
446
+
447
+ function timePartsToNumber(part1: string, part2: string): number {
448
+ const num = Number(part1 + part2);
449
+ return Number.isNaN(num) ? 0 : num;
450
+ }
451
+
452
+ function TimePicker(props: TimePickerProps) {
453
+ const {onChange, value, open, onClose, timeMode} = props;
454
+ const [timeParts, setTimeParts] = useState<TimePartValues>(dateToTimeParts(value));
455
+ const secondHourMax = useMemo(() => {
456
+ if (Number(timeParts[0]) === 2) return 3;
457
+ return 9;
458
+ }, timeParts);
459
+ const compID = useRef(generateComponentId());
460
+
461
+ function onClickNow() {
462
+ setTimeParts(dateToTimeParts(new IDate()));
463
+ }
464
+
465
+ function onSubmit() {
466
+ console.log(timeParts);
467
+ const hours = timePartsToNumber(timeParts[0], timeParts[1]);
468
+ const minutes = timePartsToNumber(timeParts[2], timeParts[3]);
469
+ const seconds = timePartsToNumber(timeParts[4], timeParts[5]);
470
+ console.log(hours, minutes, seconds);
471
+ const _value = value?.clone() ?? new IDate();
472
+ onChange(_value.set({hours, minutes, seconds}));
473
+ onClose();
474
+ }
475
+
476
+ function onKeyDown(index: number) {
477
+ return function (evt: React.KeyboardEvent) {
478
+ const {key} = evt;
479
+ if (/\d/.test(key) && index < 5) {
480
+ onChangePickerPart(index)(key);
481
+ defer(() => onFocusElement(index + 1));
482
+ }
483
+ if (key.toLowerCase() === 'backspace')
484
+ if ((evt.target as HTMLInputElement).value) {
485
+ onChangePickerPart(index)('');
486
+ } else if (index > 0) {
487
+ onChangePickerPart(index - 1)('');
488
+ defer(() => onFocusElement(index - 1));
489
+ }
490
+ }
491
+ }
492
+
493
+ function onChangePickerPart(index: number) {
494
+ return function (value: string) {
495
+ const parts = [...timeParts];
496
+ parts[index] = value;
497
+ setTimeParts(parts as TimePartValues);
498
+ }
499
+ }
500
+
501
+ function onFocusElement(index: number) {
502
+ const input = document.getElementById(`${compID.current}-${index}-input`);
503
+ input?.focus();
504
+ }
505
+
506
+ function renderTimeControls() {
507
+ const renderHours = [DatePickerTimeMode.FULL, DatePickerTimeMode.HOURS_MINUTES, DatePickerTimeMode.HOURS_ONLY].includes(timeMode);
508
+ const renderMinutes = [DatePickerTimeMode.FULL, DatePickerTimeMode.HOURS_MINUTES, DatePickerTimeMode.MINUTES_ONLY].includes(timeMode);
509
+ const renderSeconds = [DatePickerTimeMode.FULL, DatePickerTimeMode.SECONDS_ONLY].includes(timeMode);
510
+ const timeControls: Array<React.ReactElement> = [];
511
+ if (renderHours) timeControls.push(renderTimeControl([0, 1], 2, secondHourMax, true));
512
+ if (renderMinutes) timeControls.push(renderTimeControl([2, 3], 5, 9, !renderHours));
513
+ if (renderSeconds) timeControls.push(renderTimeControl([4, 5], 5, 9, !renderHours && !renderMinutes));
514
+ return (
515
+ <div className='time-controls'>
516
+ {timeControls.map((control: React.ReactElement, index: number) => {
517
+ return (
518
+ <Fragment key={index}>
519
+ {control}
520
+ {index < timeControls.length - 1 && <span className='sep'>:</span>}
521
+ </Fragment>
522
+ )
523
+ })}
524
+ </div>
525
+ )
526
+ }
527
+
528
+ function renderTimeControl(timePartIndexes: [number, number], firstMax: number, secondMax: number = 9,
529
+ autoFocus: boolean = false) {
530
+ return (
531
+ <div className='time-control'>
532
+ <DvrdNumberInput value={timeParts[timePartIndexes[0]]} onChange={voidFunction}
533
+ inputProps={{min: 0, max: firstMax, tabIndex: timePartIndexes[0] + 1, maxLength: 1}}
534
+ className='time-control-part' id={`${compID.current}-${timePartIndexes[0]}`}
535
+ onKeyDown={onKeyDown(timePartIndexes[0])} placeholder='0' autoFocus={autoFocus}
536
+ autoSelect wholeNumbers/>
537
+ <DvrdNumberInput value={timeParts[timePartIndexes[1]]} onChange={voidFunction}
538
+ inputProps={{min: 0, max: secondMax, tabIndex: timePartIndexes[1] + 1, maxLength: 1}}
539
+ onKeyDown={onKeyDown(timePartIndexes[1])} className='time-control-part'
540
+ id={`${compID.current}-${timePartIndexes[1]}`} placeholder='0' autoSelect
541
+ wholeNumbers/>
542
+ </div>
543
+ )
544
+ }
545
+
546
+ useEffect(() => {
547
+ setTimeParts(dateToTimeParts(value));
548
+ }, [value]);
549
+
550
+ return (
551
+ <WithBackground active={open} onClose={onClose}>
552
+ <div className='picker time'>
553
+ <div className='switcher'>
554
+ <label className='switcher-label'>Tijdstip</label>
555
+ </div>
556
+ {renderTimeControls()}
557
+ <div className='actions-container'>
558
+ <DvrdButton onClick={onClickNow} label='Huidig tijdstip' secondary className='action'/>
559
+ <DvrdButton onClick={onSubmit} label='Oké' className='action'/>
560
+ </div>
561
+ </div>
562
+ </WithBackground>
563
+ );
404
564
  }
@@ -293,6 +293,31 @@
293
293
  }
294
294
  }
295
295
 
296
+ .picker.time {
297
+ width: 20rem;
298
+
299
+ .time-controls {
300
+ display: flex;
301
+ column-gap: .5rem;
302
+ align-items: center;
303
+ justify-content: center;
304
+ padding: 1rem 0;
305
+
306
+ .time-control {
307
+ display: flex;
308
+ column-gap: .25rem;
309
+
310
+ .time-control-part {
311
+ width: 2.5rem;
312
+
313
+ .dvrd-input {
314
+ text-align: center;
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+
296
321
  .error-label {
297
322
  position: absolute;
298
323
  bottom: 0;
@@ -11,7 +11,7 @@ import defer from 'lodash.defer';
11
11
 
12
12
 
13
13
  interface Props {
14
- onChange: (value: SelectValueType, evt: React.MouseEvent) => void;
14
+ onChange: (value: SelectValueType, evt?: React.MouseEvent | React.ChangeEvent) => void;
15
15
  items: Array<GroupedSelectItem>;
16
16
  value: SelectValueType;
17
17
  label?: string;
@@ -77,8 +77,7 @@ function DVRDGroupedSelect(props: Props, ref: ForwardedRef<GroupedSelectRef>) {
77
77
  return function (evt: React.MouseEvent) {
78
78
  evt.stopPropagation();
79
79
  if (item.value !== undefined) {
80
- if (item.value !== value)
81
- onChange(item.value, evt);
80
+ onChange(item.value, evt);
82
81
  onToggleOpen(false);
83
82
  }
84
83
  }
@@ -169,7 +168,8 @@ function DVRDGroupedSelect(props: Props, ref: ForwardedRef<GroupedSelectRef>) {
169
168
  <div id={`dvrd-grouped-item-${item.value}`} key={index}
170
169
  className={classNames('dvrd-grouped-item', itemClassName)}
171
170
  onClick={onSelectItem(item)}>
172
- {isElement ? renderCustomLabel(item.label as React.ReactElement, padding, isGroupHead, isSelectable) :
171
+ {isElement ? renderCustomLabel(item.label as React.ReactElement, padding, isGroupHead,
172
+ isSelectable, isSelected) :
173
173
  <label className={classNames('grouped-item-label', isGroupHead && 'group-head',
174
174
  isSelectable && 'selectable', (isSelected && highlightSelected) && 'selected')}
175
175
  style={{paddingLeft: padding}}>{item.label}</label>}
@@ -183,12 +183,14 @@ function DVRDGroupedSelect(props: Props, ref: ForwardedRef<GroupedSelectRef>) {
183
183
  }
184
184
  }
185
185
 
186
- function renderCustomLabel(label: React.ReactElement, padding: string, isGroupHead: boolean, isSelectable: boolean) {
186
+ function renderCustomLabel(label: React.ReactElement, padding: string, isGroupHead: boolean, isSelectable: boolean,
187
+ isSelected: boolean) {
187
188
  const style = label.props?.style ?? {};
188
189
  return React.cloneElement(label, {
189
190
  ...label.props,
190
191
  style: {...style, paddingLeft: padding},
191
192
  className: classNames('grouped-item-label', isGroupHead && 'group-head', isSelectable && 'selectable',
193
+ (isSelected && highlightSelected && 'selected'),
192
194
  label.props?.className)
193
195
  });
194
196
  }
@@ -27,6 +27,7 @@ interface Props {
27
27
  onFocus?: FocusEventHandler;
28
28
  onBlur?: FocusEventHandler;
29
29
  onKeyDown: KeyboardEventHandler;
30
+ onKeyUp?: KeyboardEventHandler;
30
31
  onClearInput?: MouseEventHandler;
31
32
  value: string | number;
32
33
  disabled?: boolean;
@@ -52,7 +53,7 @@ export default function DvrdInput(props: Props) {
52
53
  const {
53
54
  area, id, label, labelClassName, errorClassName, inputClassName, className, ornamentClassName, inputProps,
54
55
  disabled, onBlur, value, onChange, onFocus, autoSelect, autoFocus, onClearInput, noResize, onKeyDown, fullWidth,
55
- ornaments, error
56
+ ornaments, error, onKeyUp
56
57
  } = props;
57
58
  const inputRef = useRef<HTMLInputElement & HTMLTextAreaElement>(null);
58
59
  const [active, setActive] = useState(props.autoFocus ?? false);
@@ -128,7 +129,7 @@ export default function DvrdInput(props: Props) {
128
129
  const elementProps = {
129
130
  ...inputProps, value, onChange, onFocus: onFocusInput, onBlur: onBlurInput, onKeyDown, disabled,
130
131
  className: classNames('dvrd-input', inputClassName, noResize && 'no-resize'), id: `${id}-input`,
131
- ref: inputRef,
132
+ ref: inputRef, onKeyUp,
132
133
  };
133
134
  if (area) return <textarea {...elementProps}/>;
134
135
  return <input {...elementProps}/>
@@ -22,6 +22,7 @@ export interface InputControllerProps {
22
22
  onFocus?: FocusEventHandler;
23
23
  onBlur?: FocusEventHandler;
24
24
  onKeyDown?: KeyboardEventHandler;
25
+ onKeyUp?: KeyboardEventHandler;
25
26
  onEnter?: KeyboardEventHandler;
26
27
  onClearInput?: MouseEventHandler;
27
28
  value: string | number;
@@ -53,7 +54,7 @@ export default function DvrdInputController(props: InputControllerProps) {
53
54
  const {
54
55
  disabled, onChange, onEnter, onKeyDown, inputProps, placeholder, type, max, min, step, onFocus, onBlur,
55
56
  autoFocus, error, label, ornaments, className, inputClassName, labelClassName, ornamentClassName, fullWidth,
56
- errorClassName, autoSelect, area, noResize, onClearInput, unControlled
57
+ errorClassName, autoSelect, area, noResize, onClearInput, unControlled, onKeyUp
57
58
  } = props,
58
59
  componentId = useRef(generateComponentId(props.id)),
59
60
  [value, setValue] = useState(props.value);
@@ -92,6 +93,6 @@ export default function DvrdInputController(props: InputControllerProps) {
92
93
  inputProps={getInputProps()} className={className} inputClassName={inputClassName}
93
94
  labelClassName={labelClassName} ornamentClassName={ornamentClassName} fullWidth={fullWidth}
94
95
  errorClassName={errorClassName} id={componentId.current} onBlur={onBlur} autoSelect={autoSelect}
95
- area={area} noResize={noResize} onClearInput={onClearInput}/>
96
+ area={area} noResize={noResize} onClearInput={onClearInput} onKeyUp={onKeyUp}/>
96
97
  );
97
98
  }
@@ -12,6 +12,7 @@ interface Props {
12
12
  onFocus?: FocusEventHandler;
13
13
  onBlur?: FocusEventHandler;
14
14
  onKeyDown?: KeyboardEventHandler;
15
+ onKeyUp?: KeyboardEventHandler;
15
16
  onEnter?: KeyboardEventHandler;
16
17
  value: string | number;
17
18
  label?: string;
@@ -26,7 +26,7 @@ export const isNotNull = (obj: any): boolean => {
26
26
  export const isNull = (obj: any): boolean => !isNotNull(obj);
27
27
 
28
28
  export const hasHover = (element: HTMLElement | null): boolean => {
29
- if(!element) return false;
29
+ if (!element) return false;
30
30
  if (isNotNull(element) && element.parentElement !== null) {
31
31
  return element.parentElement.querySelector(':hover') === element;
32
32
  }
@@ -93,6 +93,10 @@ export const calculateTextWidth = (text: string, fontSize?: number, fontFamily?:
93
93
 
94
94
  export const pad = (num: string | number): string => ('0' + num).slice(-2);
95
95
 
96
+ export function padNum(num: string | number, maxLength: number = 2): string {
97
+ return num.toString().padStart(maxLength, '0');
98
+ }
99
+
96
100
  export const stopPropagation = (evt: Event | React.SyntheticEvent) => {
97
101
  evt.stopPropagation();
98
102
  };
@@ -150,6 +150,8 @@ export enum SideMenuMode {COMPACT, FULL}
150
150
 
151
151
  export enum PDFDisplay {FIRST_PAGE = 'first_page', LAST_PAGE = 'last_page', ALL_PAGES = 'all_pages'}
152
152
 
153
+ export enum DatePickerTimeMode {FULL = 1, HOURS_MINUTES, HOURS_ONLY, MINUTES_ONLY, SECONDS_ONLY}
154
+
153
155
  // =========== TYPES
154
156
  export type OptionsMenuItem = {
155
157
  label: string;