@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 +4 -4
- package/package.json +1 -1
- package/src/js/pdf/settings/invoiceTable/pdfInvoiceTableSettings.tsx +3 -3
- package/src/js/pdf/settings/text/pdfTextSettings.tsx +2 -2
- package/src/js/sidebarMenu/sidebarMenu.tsx +24 -22
- package/src/js/sidebarMenu/style/sidebarMenu.scss +12 -26
- package/src/js/switch/dvrdSwitch.tsx +36 -9
- package/src/js/switch/style/dvrdSwitch.scss +12 -6
- package/src/js/textField/dvrdNumberInput.tsx +92 -106
- package/src/js/util/inputUtil.ts +21 -0
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
|
|
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
|
|
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
|
-
|
|
101
|
+
DVRDNumberInput,
|
|
102
102
|
DVRDPasswordInput,
|
|
103
103
|
Link,
|
|
104
104
|
DvrdOptionsList,
|
|
105
105
|
DvrdSelectController as DvrdSelect,
|
|
106
106
|
DVRDGroupedSelect,
|
|
107
|
-
|
|
107
|
+
DVRDSwitch,
|
|
108
108
|
DvrdHeaderController as DvrdHeader,
|
|
109
109
|
FileUpload,
|
|
110
110
|
DvrdRadioController as DvrdRadio,
|
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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,
|
|
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
|
-
<
|
|
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
|
|
8
|
+
import {SidebarItem, SideMenuMode} from '../util/interfaces';
|
|
9
9
|
import classNames from 'classnames';
|
|
10
|
-
import {AwesomeIcon, generateComponentId, isAbsoluteLink} from
|
|
11
|
-
import {ControlContext} from
|
|
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(
|
|
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 &&
|
|
94
|
-
|
|
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={
|
|
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
|
-
|
|
67
|
-
.item-label {
|
|
69
|
+
&.active {
|
|
68
70
|
visibility: visible;
|
|
69
71
|
opacity: 1;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
.
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
20
|
-
const context = useContext(ControlContext)
|
|
21
|
-
|
|
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 (
|
|
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={
|
|
47
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
32
|
-
wholeNumbers
|
|
33
|
+
multipleSeparators?: boolean;
|
|
34
|
+
wholeNumbers?: boolean;
|
|
33
35
|
asNumber?: boolean;
|
|
34
|
-
asCurrency
|
|
36
|
+
asCurrency?: boolean;
|
|
35
37
|
autoSelect?: boolean;
|
|
36
38
|
unControlled?: boolean;
|
|
39
|
+
disableSymbols?: boolean;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
const {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 (
|
|
92
|
+
if (wholeNumbers && /[.,]/.test(key))
|
|
71
93
|
evt.preventDefault();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|