@dvrd/dvr-controls 1.0.82 → 1.0.84

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/index.ts CHANGED
@@ -47,13 +47,13 @@ import Media from './src/js/media/media';
47
47
  import DvrdButton, {DvrdButtonProps} from './src/js/button/dvrdButton';
48
48
  import DvrdDatePicker, {DVRDDatePickerRef} from './src/js/date/dvrdDatePicker';
49
49
  import DvrdInputController from './src/js/textField/dvrdInputController';
50
- import DvrdNumberInput from './src/js/textField/dvrdNumberInput';
50
+ import DVRDNumberInput from './src/js/textField/dvrdNumberInput';
51
51
  import DVRDPasswordInput from './src/js/textField/dvrdPasswordInput';
52
52
  import Link from './src/js/link/link';
53
53
  import DvrdOptionsList from './src/js/optionsList/dvrdOptionsList';
54
54
  import DvrdSelectController from './src/js/select/dvrdSelectController';
55
55
  import DVRDGroupedSelect, { GroupedSelectRef } from './src/js/select/dvrdGroupedSelect';
56
- import DvrdSwitch from './src/js/switch/dvrdSwitch';
56
+ import DVRDSwitch from './src/js/switch/dvrdSwitch';
57
57
  import DvrdHeaderController from './src/js/header/v2/dvrdHeaderController';
58
58
  import FileUpload from './src/js/fileUpload/fileUpload';
59
59
  import DvrdRadioController from './src/js/radio/dvrdRadioController';
@@ -98,13 +98,13 @@ export {
98
98
  DvrdDatePicker,
99
99
  DVRDDatePickerRef,
100
100
  DvrdInputController as DvrdInput,
101
- DvrdNumberInput,
101
+ DVRDNumberInput,
102
102
  DVRDPasswordInput,
103
103
  Link,
104
104
  DvrdOptionsList,
105
105
  DvrdSelectController as DvrdSelect,
106
106
  DVRDGroupedSelect,
107
- DvrdSwitch,
107
+ DVRDSwitch,
108
108
  DvrdHeaderController as DvrdHeader,
109
109
  FileUpload,
110
110
  DvrdRadioController as DvrdRadio,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvrd/dvr-controls",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
4
4
  "description": "Custom web controls",
5
5
  "main": "index.ts",
6
6
  "files": [
@@ -8,8 +8,8 @@ import PdfInvoiceTable from "../../invoiceTable/pdfInvoiceTable";
8
8
  import {debug} from "../../../util/miscUtil";
9
9
  import {ElementPosition} from "../../../util/interfaces";
10
10
  import {fontItems} from "../../../util/pdfUtil";
11
- import {ColorPicker, DvrdNumberInput, DvrdSelect} from "../../../../../index";
12
11
  import IconButton, {IconButtonType} from "../buttons/iconButton";
12
+ import {ColorPicker, DVRDNumberInput, DvrdSelect} from "../../../../../index";
13
13
 
14
14
  interface Props {
15
15
  element: PdfInvoiceTable;
@@ -116,7 +116,7 @@ export default class PdfInvoiceTableSettings extends PureComponent<Props, State>
116
116
  renderWidth = (value: number, index: number) => {
117
117
  const title = TITLES[index];
118
118
  return (
119
- <DvrdNumberInput key={index} value={value.toString()} onChange={this.onChangeWidth(index)} label={title}
119
+ <DVRDNumberInput key={index} value={value.toString()} onChange={this.onChangeWidth(index)} label={title}
120
120
  ornaments={{
121
121
  element: <span style={{cursor: 'text'}}>%</span>,
122
122
  placement: ElementPosition.RIGHT,
@@ -145,7 +145,7 @@ export default class PdfInvoiceTableSettings extends PureComponent<Props, State>
145
145
  <label className='settings-label'>Tekstgrootte</label>
146
146
  <DvrdSelect items={fontItems} value={settings.font} onChange={element.changeSetting('font')}
147
147
  label='Lettertype' optionsContainerHeight='15rem' unControlled/>
148
- <DvrdNumberInput value={settings.fontSize} onChange={element.changeSetting('fontSize')}
148
+ <DVRDNumberInput value={settings.fontSize} onChange={element.changeSetting('fontSize')}
149
149
  asNumber
150
150
  ornaments={{
151
151
  element: <span style={{cursor: 'text'}}>px</span>,
@@ -14,7 +14,7 @@ import {
14
14
  PDFTextVariables,
15
15
  SelectItemShape
16
16
  } from "../../../util/interfaces";
17
- import {ColorPicker, DvrdNumberInput, DvrdSelect, fontItems, getPdfVariables} from "../../../../../index";
17
+ import {ColorPicker, DVRDNumberInput, DvrdSelect, fontItems, getPdfVariables} from "../../../../../index";
18
18
  import classNames from 'classnames';
19
19
  import IconButton, {IconButtonType} from "../buttons/iconButton";
20
20
 
@@ -177,7 +177,7 @@ export default class PdfTextSettings extends PureComponent<Props, State> {
177
177
  <div className='font-settings'>
178
178
  <DvrdSelect items={fontItems} value={settings.font} onChange={element.changeSetting('font')}
179
179
  label='Lettertype' optionsContainerHeight='15rem' unControlled/>
180
- <DvrdNumberInput value={settings.fontSize} onChange={element.changeSetting('fontSize')}
180
+ <DVRDNumberInput value={settings.fontSize} onChange={element.changeSetting('fontSize')}
181
181
  label='Tekstgrootte' className='font-size'
182
182
  ornaments={{
183
183
  element: <span style={{cursor: 'text'}}>px</span>,
@@ -5,10 +5,10 @@ import './style/sidebarMenu.scss';
5
5
 
6
6
  import React, {MouseEventHandler, ReactNode, useContext, useEffect, useRef, useState} from 'react';
7
7
  import {NavLink, useLocation} from 'react-router-dom';
8
- import {SidebarItem, SideMenuMode} from "../util/interfaces";
8
+ import {SidebarItem, SideMenuMode} from '../util/interfaces';
9
9
  import classNames from 'classnames';
10
- import {AwesomeIcon, generateComponentId, isAbsoluteLink} from "../../../index";
11
- import {ControlContext} from "../util/controlContext";
10
+ import {AwesomeIcon, generateComponentId, isAbsoluteLink} from '../../../index';
11
+ import {ControlContext} from '../util/controlContext';
12
12
  import defer from 'lodash.defer';
13
13
  import {IconName} from '@fortawesome/fontawesome-svg-core';
14
14
 
@@ -29,7 +29,7 @@ function getTopItems(items: SidebarItem[]): SidebarItem[] {
29
29
  }
30
30
 
31
31
  function getBottomItems(items: SidebarItem[]): SidebarItem[] {
32
- return items.filter((item: SidebarItem) => item.onBottom)
32
+ return items.filter((item: SidebarItem) => item.onBottom);
33
33
  }
34
34
 
35
35
  function getActiveItem(items: SidebarItem[], pathname: string): SidebarItem | null {
@@ -52,7 +52,8 @@ function itemIsActive(item: SidebarItem, pathname: string): boolean {
52
52
 
53
53
  function itemHasActiveId(item: SidebarItem, activeId: string): boolean {
54
54
  if (item.id === activeId) return true;
55
- else if (Array.isArray(item.children) && item.children.find((child: SidebarItem) => itemHasActiveId(child, activeId))) return true;
55
+ else if (Array.isArray(item.children) && item.children.find(
56
+ (child: SidebarItem) => itemHasActiveId(child, activeId))) return true;
56
57
  return false;
57
58
  }
58
59
 
@@ -77,7 +78,7 @@ export default function SidebarMenu(props: Props) {
77
78
  if (_route && !isAbsoluteLink(_route))
78
79
  setActiveItem(item.id);
79
80
  onClickItem(item)(evt);
80
- }
81
+ };
81
82
  }
82
83
 
83
84
  function renderItem(isChild: boolean = false) {
@@ -87,11 +88,12 @@ export default function SidebarMenu(props: Props) {
87
88
  cls = classNames(className, mode === SideMenuMode.COMPACT ? 'side-bar-item' : 'side-bar-item-full',
88
89
  isChild && 'child', children !== undefined && 'with-children');
89
90
 
90
- const content = (
91
+ const content = (active: boolean) => (
91
92
  <>
92
- {renderIcon(isChild, label, icon)}
93
- {mode === SideMenuMode.COMPACT && <div className='active-indicator'/>}
94
- <span className='item-label'>{label}</span>
93
+ {renderIcon(isChild, label, active, icon)}
94
+ {mode === SideMenuMode.COMPACT &&
95
+ <div className={classNames('active-indicator', active && 'active')}/>}
96
+ <span className={classNames('item-label', active && 'active')}>{label}</span>
95
97
  {children !== undefined && (
96
98
  <>
97
99
  {isActive && <div className='line'/>}
@@ -103,15 +105,15 @@ export default function SidebarMenu(props: Props) {
103
105
 
104
106
  if (asNavLink) return (
105
107
  <NavLink key={id} to={item.route as string} className={cls} id={id}>
106
- {content}
108
+ {({isActive}) => content(isActive)}
107
109
  </NavLink>
108
110
  );
109
111
  return (
110
- <div key={id} className={classNames(cls, isActive && 'active')} onClick={_onClickItem(item)} id={id}>
111
- {content}
112
+ <div key={id} className={cls} onClick={_onClickItem(item)} id={id}>
113
+ {content(isActive)}
112
114
  </div>
113
- )
114
- }
115
+ );
116
+ };
115
117
  }
116
118
 
117
119
  function renderChildren(isActive: boolean) {
@@ -121,12 +123,12 @@ export default function SidebarMenu(props: Props) {
121
123
  <div className='children-container' style={{display}}>
122
124
  {children.map(renderItem(true))}
123
125
  </div>
124
- )
125
- }
126
+ );
127
+ };
126
128
  }
127
129
 
128
- function renderIcon(isChild: boolean, label: string, icon?: IconName | ReactNode): ReactNode {
129
- const className = 'item-icon'
130
+ function renderIcon(isChild: boolean, label: string, active: boolean, icon?: IconName | ReactNode): ReactNode {
131
+ const className = classNames('item-icon', active && 'active');
130
132
  if (icon) {
131
133
  if (typeof icon === 'string') return <AwesomeIcon name={icon as IconName} className={className}/>;
132
134
  else if (React.isValidElement(icon))
@@ -135,7 +137,7 @@ export default function SidebarMenu(props: Props) {
135
137
  }
136
138
  if (label.length) {
137
139
  if (isChild) return <span/>;
138
- return <span className={classNames(className, 'letter')}>{label[0]}</span>
140
+ return <span className={classNames(className, 'letter')}>{label[0]}</span>;
139
141
  }
140
142
  return null;
141
143
  }
@@ -162,7 +164,7 @@ export default function SidebarMenu(props: Props) {
162
164
  }, []);
163
165
 
164
166
  useEffect(() => {
165
- _setActiveItem()
167
+ _setActiveItem();
166
168
  }, [location]);
167
169
 
168
170
  return (
@@ -175,5 +177,5 @@ export default function SidebarMenu(props: Props) {
175
177
  {getBottomItems(items).map(renderItem())}
176
178
  </div>
177
179
  </div>
178
- )
180
+ );
179
181
  }
@@ -34,6 +34,10 @@
34
34
  color: $color-gray-4;
35
35
  font-size: 1.5rem;
36
36
  transition: color .2s ease-in-out;
37
+
38
+ &.active {
39
+ color: var(--base-color);
40
+ }
37
41
  }
38
42
 
39
43
  .item-label {
@@ -61,24 +65,18 @@
61
65
  visibility: hidden;
62
66
  opacity: 0;
63
67
  transition: visibility .2s ease-in-out, opacity .2s ease-in-out;
64
- }
65
68
 
66
- &:hover {
67
- .item-label {
69
+ &.active {
68
70
  visibility: visible;
69
71
  opacity: 1;
70
72
  }
71
73
  }
72
74
 
73
- &.active {
74
- .active-indicator {
75
+ &:hover {
76
+ .item-label {
75
77
  visibility: visible;
76
78
  opacity: 1;
77
79
  }
78
-
79
- .item-icon {
80
- color: var(--base-color);
81
- }
82
80
  }
83
81
  }
84
82
 
@@ -119,11 +117,11 @@
119
117
  transition: color .2s ease-in-out;
120
118
  white-space: nowrap;
121
119
  user-select: none;
122
- //
123
- //&.active {
124
- // font-weight: 600;
125
- // color: black;
126
- //}
120
+
121
+ &.active {
122
+ font-weight: 600;
123
+ color: black;
124
+ }
127
125
  }
128
126
 
129
127
  .line {
@@ -164,18 +162,6 @@
164
162
  &.with-children {
165
163
  margin-bottom: -.5rem;
166
164
  }
167
-
168
- &.active {
169
- .item-icon {
170
- color: var(--base-color);
171
- opacity: 1;
172
- }
173
-
174
- .item-label {
175
- font-weight: 600;
176
- color: black;
177
- }
178
- }
179
165
  }
180
166
  }
181
167
  }
@@ -4,7 +4,7 @@
4
4
  import './style/dvrdSwitch.scss';
5
5
 
6
6
  import classNames from 'classnames';
7
- import React, {useContext} from 'react';
7
+ import React, {useContext, useMemo} from 'react';
8
8
  import {ControlContext} from "../util/controlContext";
9
9
  import {convertColor} from "../util/colorUtil";
10
10
 
@@ -12,13 +12,29 @@ interface Props {
12
12
  onChange: (value: boolean, evt?: React.MouseEvent | React.ChangeEvent) => void;
13
13
  value: boolean;
14
14
  className?: string;
15
+ labelClassName?: string;
15
16
  disabled?: boolean;
16
17
  activeColor?: string;
18
+ trackColor?: string;
19
+ baseColor?: string;
20
+ contrastColor?: string;
21
+ labels?: [string, string]; // Labels placed to the left and right of the switch
22
+ label?: string; // Label placed to the right of the switch
17
23
  }
18
24
 
19
- export default function DvrdSwitch(props: Props) {
20
- const context = useContext(ControlContext),
21
- {className, onChange, disabled, value, activeColor} = props;
25
+ export default function DVRDSwitch(props: Props) {
26
+ const context = useContext(ControlContext);
27
+ const {
28
+ onChange, disabled, value, activeColor, labels, label, trackColor, baseColor, contrastColor,
29
+ labelClassName
30
+ } = props;
31
+ const className = useMemo(() => {
32
+ const names: Array<string | undefined> = ['dvrd-switch', props.className];
33
+ if (!!labels || !!label) names.push('with-labels');
34
+ if (value) names.push('active');
35
+ if (disabled) names.push('disabled');
36
+ return classNames(names);
37
+ }, [props.className, labels, label, value, disabled]);
22
38
 
23
39
  function onToggle(evt: React.MouseEvent) {
24
40
  if (!disabled)
@@ -27,10 +43,11 @@ export default function DvrdSwitch(props: Props) {
27
43
 
28
44
  function getTrackColor(): string {
29
45
  let color: string;
30
- if (disabled) color = 'color-gray-4';
46
+ if (trackColor) color = trackColor;
47
+ else if (disabled) color = 'color-gray-4';
31
48
  else if (value) {
32
49
  if (activeColor) color = activeColor;
33
- else color = context.baseColor;
50
+ else color = baseColor ?? context.baseColor;
34
51
  } else color = 'color-gray-7';
35
52
  return convertColor(color);
36
53
  }
@@ -38,16 +55,26 @@ export default function DvrdSwitch(props: Props) {
38
55
  function getHandleColor(): string {
39
56
  let color: string;
40
57
  if (disabled) color = 'color-gray-3';
41
- else color = context.contrastColor;
58
+ else color = contrastColor ?? context.contrastColor;
42
59
  return convertColor(color);
43
60
  }
44
61
 
62
+ function renderLabel(position?: 'left' | 'right') {
63
+ const _label = position === 'left' ? labels?.[0] : position === 'right' ? labels?.[1] : label;
64
+ if(!_label) return null;
65
+ return (
66
+ <label className={classNames('dvrd-switch-label', position, labelClassName)}>{_label}</label>
67
+ );
68
+ }
69
+
45
70
  return (
46
- <div className={classNames('dvrd-switch', value && 'active', disabled && 'disabled', className)}
47
- onClick={onToggle}>
71
+ <div className={className} onClick={onToggle}>
72
+ {renderLabel('left')}
48
73
  <div className='track' style={{backgroundColor: getTrackColor()}}>
49
74
  <div className='handle' style={{backgroundColor: getHandleColor()}}/>
50
75
  </div>
76
+ {renderLabel('right')}
77
+ {renderLabel()}
51
78
  </div>
52
79
  )
53
80
  }
@@ -7,6 +7,7 @@
7
7
  .dvrd-switch {
8
8
  $track-height: 14px;
9
9
  $handle-size: 18px;
10
+ cursor: pointer;
10
11
 
11
12
  .track {
12
13
  width: $handle-size * 1.3;
@@ -23,12 +24,21 @@
23
24
  border-radius: 100%;
24
25
  width: $handle-size;
25
26
  height: $handle-size;
26
- cursor: pointer;
27
27
  transition: transform .2s ease-in-out, left .2s ease-in-out;
28
28
  will-change: left;
29
29
  }
30
30
  }
31
31
 
32
+ .dvrd-switch-label {
33
+ user-select: none;
34
+ }
35
+
36
+ &.with-labels {
37
+ display: flex;
38
+ column-gap: 1rem;
39
+ align-items: center;
40
+ }
41
+
32
42
  &.active {
33
43
  .track {
34
44
  .handle {
@@ -38,10 +48,6 @@
38
48
  }
39
49
 
40
50
  &.disabled {
41
- .track {
42
- .handle {
43
- cursor: default;
44
- }
45
- }
51
+ cursor: default;
46
52
  }
47
53
  }
@@ -2,10 +2,12 @@
2
2
  * Copyright (c) 2024. Dave van Rijn Development
3
3
  */
4
4
 
5
- import React, {FocusEventHandler, InputHTMLAttributes, KeyboardEventHandler, PureComponent} from 'react';
5
+ import React, {FocusEventHandler, InputHTMLAttributes, KeyboardEventHandler, useEffect, useRef, useState} from 'react';
6
+ import {generateComponentId} from '../util/componentUtil';
6
7
  import {ChangeFunction, ErrorType, OrnamentShape} from '../util/interfaces';
7
8
  import {roundTo} from '../util/miscUtil';
8
9
  import DvrdInputController from "./dvrdInputController";
10
+ import {isPasting, positionIsAfterComma, textIsSelected} from "../util/inputUtil";
9
11
 
10
12
  interface Props {
11
13
  onChange: ChangeFunction<string | number>;
@@ -28,130 +30,114 @@ interface Props {
28
30
  error?: ErrorType;
29
31
  id?: string;
30
32
  placeholder?: string;
31
- multipleSeparators: boolean;
32
- wholeNumbers: boolean;
33
+ multipleSeparators?: boolean;
34
+ wholeNumbers?: boolean;
33
35
  asNumber?: boolean;
34
- asCurrency: boolean;
36
+ asCurrency?: boolean;
35
37
  autoSelect?: boolean;
36
38
  unControlled?: boolean;
39
+ disableSymbols?: boolean;
37
40
  }
38
41
 
39
- // TODO Add option to disable symbols (+,-)
42
+ export default function DVRDNumberInput(props: Props) {
43
+ const {
44
+ onKeyDown, inputProps, asCurrency, asNumber, wholeNumbers, disableSymbols, onChange, value, unControlled
45
+ } = props;
46
+ const [innerValue, setInnerValue] = useState(value);
47
+ const id = useRef(generateComponentId(props.id));
40
48
 
41
- interface State {
42
- value: number | string;
43
- }
49
+ function _onKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
50
+ const {key} = evt,
51
+ numValue = Number(value);
52
+ // Allow only number and simple edit keys
53
+ const editKeys = /\d|Backspace|ArrowRight|ArrowLeft|ArrowUp|ArrowDown|Escape|Esc|Enter|[,.\-+]|Tab/;
54
+ if (!editKeys.test(key) && !isPasting(evt))
55
+ evt.preventDefault();
44
56
 
45
- export default class DvrdNumberInput extends PureComponent<Props, State> {
46
- static defaultProps = {
47
- asCurrency: false,
48
- wholeNumbers: false,
49
- multipleSeparators: false,
50
- }
57
+ // Handle Arrow events
58
+ if (['ArrowUp', 'ArrowDown'].includes(key)) {
59
+ evt.preventDefault();
60
+ if (!isNaN(numValue))
61
+ return onArrowEvent(evt, numValue);
62
+ }
51
63
 
52
- constructor(props: Props) {
53
- super(props);
54
- this.state = {
55
- value: props.value,
56
- };
64
+ // Handle event if no selected text
65
+ onKeyDownSelected(evt);
66
+
67
+ if (onKeyDown)
68
+ onKeyDown(evt);
57
69
  }
58
70
 
71
+ function onArrowEvent(evt: React.KeyboardEvent<HTMLInputElement>, numValue: number) {
72
+ const {key} = evt,
73
+ step = Number(inputProps?.step || asCurrency ? .01 : 1);
74
+ if (key === 'ArrowUp')
75
+ numValue += step;
76
+ else if (key === 'ArrowDown')
77
+ numValue -= step;
78
+ let stringValue = numValue.toString();
79
+ if (asCurrency) stringValue = roundTo(numValue, 2);
80
+ if (asNumber) change(Number(stringValue));
81
+ else change(stringValue);
82
+ }
59
83
 
60
- onKeyDown = (evt: React.KeyboardEvent) => {
61
- const {onKeyDown, asCurrency, wholeNumbers, inputProps, asNumber, multipleSeparators} = this.props;
62
- const {value} = this.state;
63
- const {key, target} = evt;
64
- let numValue = Number(value);
65
- // Allow only number and simple edit keys
66
- if (!/\d|Backspace|ArrowRight|ArrowLeft|ArrowUp|ArrowDown|Escape|Esc|Enter|[,.\-+]|Tab|Delete|Del/.test(key) &&
67
- !this.isPasting(evt))
84
+ function onKeyDownSelected(evt: React.KeyboardEvent<HTMLInputElement>) {
85
+ if (textIsSelected(evt)) return;
86
+ const {currentTarget, key} = evt,
87
+ position = currentTarget.selectionStart;
88
+ if (/[,.]/.test(key) && /[,.]/.test(value.toString()) && asCurrency)
89
+ // Prevent double points or commas
68
90
  evt.preventDefault();
69
91
 
70
- if (key === 'ArrowUp') {
92
+ if (wholeNumbers && /[.,]/.test(key))
71
93
  evt.preventDefault();
72
- if (!isNaN(numValue)) {
73
- const step = Number(inputProps?.step || asCurrency ? .01 : 1);
74
- numValue += step;
75
- let stringValue = numValue.toString();
76
- if (asCurrency) stringValue = roundTo(numValue, 2);
77
- if (asNumber) this.change(Number(stringValue), evt);
78
- else this.change(stringValue, evt);
79
- }
80
- return;
81
- }
82
- if (key === 'ArrowDown') {
94
+ else if (asCurrency && /[.,]\d{2}/.test(value.toString()) && /\d/.test(key) &&
95
+ positionIsAfterComma(position, value.toString()))
96
+ // Prevent more than 2 decimals when working with currencies
83
97
  evt.preventDefault();
84
- if (!isNaN(numValue)) {
85
- const step = Number(inputProps?.step || asCurrency ? .01 : 1);
86
- numValue -= step;
87
- let stringValue = numValue.toString();
88
- if (asCurrency) stringValue = roundTo(numValue, 2);
89
- if (asNumber) this.change(Number(stringValue), evt);
90
- else this.change(stringValue, evt);
91
- }
92
- return;
93
- }
94
98
 
95
- const textIsSelected = this.textIsSelected(evt), position = (target as HTMLInputElement).selectionStart;
96
- if (!textIsSelected) {
97
- if (/[,.]/.test(key) && /[,.]/.test(value.toString()) && !multipleSeparators)
98
- // Prevent double points or commas
99
- evt.preventDefault();
100
-
101
- if (wholeNumbers && /[.,]/.test(key))
102
- evt.preventDefault();
103
- else if (asCurrency && /[.,]\d{2}/.test(value.toString()) && /\d/.test(key) &&
104
- this.positionIsAfterComma(position, value.toString()))
105
- // Prevent more than 2 decimals when working with currencies
106
- evt.preventDefault();
107
-
108
- if ((position && position !== 0) && /[\-+]/.test(key))
109
- // Prevent - or + sign after first position
110
- evt.preventDefault();
111
- }
112
-
113
- if (onKeyDown !== undefined && onKeyDown !== null)
114
- onKeyDown(evt);
115
- };
99
+ if (/[\-+]/.test(key) && (disableSymbols || (position && position !== 0)))
100
+ // Prevent - or + sign completely or after first position
101
+ evt.preventDefault();
102
+ }
116
103
 
117
- onChange = (value: string, evt: React.ChangeEvent) => {
118
- const {asNumber} = this.props;
104
+ function _onChange(value: string, evt: React.ChangeEvent<HTMLInputElement>) {
105
+ value = value.replace(/ /g, '');
119
106
  if (!/^-?[\d.,]*$/.test(value)) evt.preventDefault();
120
107
  else {
121
- let changeValue: string | number = value;
122
- if (value.startsWith('.') || value.startsWith(',')) value = 0 + value;
123
- if (asNumber) changeValue = Number(value.replace(',', '.'));
124
- this.change(changeValue, evt);
108
+ if (value.startsWith('.') || value.startsWith(',')) value = '0' + value;
109
+ if (wholeNumbers)
110
+ // Only allow numbers and minus sign for negative numbers
111
+ value = value.replace(/[^0-9-]/g, '').trim();
112
+ if (disableSymbols)
113
+ value = value.replace(/[\-+]/g, '').trim();
114
+ if (asNumber) {
115
+ const numValue = Number(value.replace(',', '.'));
116
+ change(numValue, evt);
117
+ } else change(value, evt);
125
118
  }
126
- };
127
-
128
- change = (value: string | number, evt: React.ChangeEvent | React.KeyboardEvent) => {
129
- this.setState({value}, () => {
130
- this.props.onChange(value, evt as React.ChangeEvent);
131
- })
132
- };
133
-
134
- isPasting = (evt: React.KeyboardEvent) => {
135
- return (evt.metaKey || evt.ctrlKey) && evt.key === 'v';
136
- };
137
-
138
- positionIsAfterComma = (position: number | null, value: string) => {
139
- if (position === null) return true;
140
- if (value.includes('.')) return position > value.indexOf('.');
141
- if (value.includes(',')) return position > value.indexOf(',');
142
- return false;
143
- };
144
-
145
- textIsSelected = (evt: React.KeyboardEvent) => {
146
- const target = evt.target as HTMLInputElement, start = target.selectionStart, end = target.selectionEnd;
147
- return start != end;
148
- };
149
-
150
- render = () => {
151
- const value = this.props.unControlled ? this.state.value : this.props.value;
152
- return (
153
- <DvrdInputController {...this.props} value={value} onChange={this.onChange} onKeyDown={this.onKeyDown}
154
- unControlled={false}/>
155
- );
119
+ return value;
120
+ }
121
+
122
+ function change(value: number | string, evt?: React.ChangeEvent<HTMLInputElement>) {
123
+ setInnerValue(value);
124
+ onChange(value, evt);
125
+ }
126
+
127
+ function focusInput() {
128
+ document.getElementById(id.current + '-input')?.focus();
156
129
  }
130
+
131
+ useEffect(() => {
132
+ if (!unControlled) setInnerValue(value);
133
+ }, [value]);
134
+
135
+ useEffect(() => {
136
+ if (!props.disabled && props.autoFocus)
137
+ focusInput();
138
+ }, [props.disabled, props.autoFocus]);
139
+
140
+ return (
141
+ <DvrdInputController {...props} id={id.current} onChange={_onChange} onKeyDown={_onKeyDown} value={innerValue}/>
142
+ );
157
143
  }
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Copyright (c) 2024. Dave van Rijn Development
3
+ */
4
+
5
+ export function isPasting(evt: React.KeyboardEvent): boolean {
6
+ return (evt.metaKey || evt.ctrlKey) && evt.key === 'v';
7
+ }
8
+
9
+ export function textIsSelected(evt: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>): boolean {
10
+ const {currentTarget} = evt,
11
+ start = currentTarget.selectionStart,
12
+ end = currentTarget.selectionEnd;
13
+ return start != end;
14
+ }
15
+
16
+ export function positionIsAfterComma(position: number | null, value: string): boolean {
17
+ if (position === null) return true;
18
+ if (value.includes('.')) return position > value.indexOf('.');
19
+ if (value.includes(',')) return position > value.indexOf(',');
20
+ return false;
21
+ }