@dvrd/dvr-controls 1.0.45 → 1.0.46
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
|
@@ -4,145 +4,133 @@
|
|
|
4
4
|
|
|
5
5
|
import './style/checkbox.scss';
|
|
6
6
|
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import {MouseEventHandler} from 'react';
|
|
7
|
+
import React, {CSSProperties, MouseEventHandler, useContext, useMemo} from 'react';
|
|
9
8
|
import classNames from 'classnames';
|
|
10
9
|
import AwesomeIcon from '../icon/awesomeIcon';
|
|
11
|
-
import {
|
|
12
|
-
import {isNotNull, isNull} from "../util/controlUtil";
|
|
10
|
+
import {convertColor, editColor, findDarkestColor} from "../util/colorUtil";
|
|
13
11
|
import {ControlContext} from "../util/controlContext";
|
|
14
|
-
import {
|
|
12
|
+
import {IconName} from '@fortawesome/fontawesome-svg-core';
|
|
13
|
+
import {ErrorType} from "../../../index";
|
|
15
14
|
|
|
16
|
-
interface
|
|
15
|
+
interface Props {
|
|
17
16
|
onCheck: MouseEventHandler;
|
|
18
|
-
onMouseEnter:
|
|
19
|
-
onMouseLeave:
|
|
20
|
-
checked: boolean
|
|
21
|
-
disabled: boolean
|
|
22
|
-
isHovered: boolean
|
|
23
|
-
label
|
|
24
|
-
labelPosition: 'left' | 'right'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
id: string
|
|
31
|
-
error:
|
|
32
|
-
checkIcon: IconName | React.ReactNode
|
|
17
|
+
onMouseEnter: MouseEventHandler;
|
|
18
|
+
onMouseLeave: MouseEventHandler;
|
|
19
|
+
checked: boolean;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
isHovered: boolean;
|
|
22
|
+
label?: string;
|
|
23
|
+
labelPosition: 'left' | 'right';
|
|
24
|
+
className?: string;
|
|
25
|
+
labelClassName?: string;
|
|
26
|
+
checkboxClassName?: string;
|
|
27
|
+
checkClassName?: string;
|
|
28
|
+
errorClassName?: string;
|
|
29
|
+
id: string;
|
|
30
|
+
error: ErrorType;
|
|
31
|
+
checkIcon: IconName | React.ReactNode;
|
|
33
32
|
baseColor?: string;
|
|
34
33
|
contrastColor?: string;
|
|
35
34
|
title?: string;
|
|
36
35
|
useDarkestColor: boolean;
|
|
36
|
+
asRadio?: boolean;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
export default function DvrdCheckbox(props: Props) {
|
|
40
|
+
const context = useContext(ControlContext);
|
|
41
|
+
const {
|
|
42
|
+
disabled, error, label, useDarkestColor, isHovered, id, onCheck, onMouseEnter, onMouseLeave, title,
|
|
43
|
+
labelPosition, checkIcon, checked, asRadio
|
|
44
|
+
} = props;
|
|
45
|
+
const className = useMemo(() => {
|
|
46
|
+
const classes: Array<string> = ['checkboxContainer'];
|
|
46
47
|
if (disabled) classes.push('disabled');
|
|
47
|
-
if (
|
|
48
|
-
if (!label
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
getLabelClass = (position: 'left' | 'right'): string => {
|
|
55
|
-
const {disabled, labelClass, error} = this.props, classes = ['checkboxLabel', position];
|
|
48
|
+
if (!!error) classes.push('error');
|
|
49
|
+
if (!label?.length) classes.push('no-label');
|
|
50
|
+
return classNames(classes, props.className);
|
|
51
|
+
}, [disabled, error, label, props.className]);
|
|
52
|
+
const labelClassName = useMemo(() => {
|
|
53
|
+
const classes: Array<string> = ['checkboxLabel'];
|
|
56
54
|
if (disabled) classes.push('disabled');
|
|
57
|
-
if (
|
|
58
|
-
classes.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
getCheckboxClass = (): string => {
|
|
63
|
-
const {disabled, checkboxClass} = this.props, classes = ['checkbox'];
|
|
55
|
+
if (!!error) classes.push('error');
|
|
56
|
+
return classNames(classes, props.labelClassName);
|
|
57
|
+
}, [disabled, error, props.labelClassName]);
|
|
58
|
+
const checkboxClassName = useMemo(() => {
|
|
59
|
+
const classes: Array<string> = ['checkbox'];
|
|
64
60
|
if (disabled) classes.push('disabled');
|
|
65
|
-
classes.push(
|
|
66
|
-
return classNames(classes);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const {disabled, checkClass, checked} = this.props, classes = ['checkbox-check'];
|
|
61
|
+
if (asRadio) classes.push('as-radio');
|
|
62
|
+
return classNames(classes, props.checkboxClassName);
|
|
63
|
+
}, [disabled, props.checkClassName]);
|
|
64
|
+
const checkClassName = useMemo(() => {
|
|
65
|
+
const classes = ['checkbox-check'];
|
|
71
66
|
if (disabled) classes.push('disabled');
|
|
72
67
|
if (checked) classes.push('checked');
|
|
73
|
-
classes.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
return classNames(classes, props.checkClassName);
|
|
69
|
+
}, [disabled, checked, props.checkClassName]);
|
|
70
|
+
const [baseColor, contrastColor] = useMemo(() => {
|
|
71
|
+
const base = convertColor(props.baseColor || context.baseColor)
|
|
72
|
+
const contrast = convertColor(props.baseColor || context.baseColor);
|
|
73
|
+
return [base, contrast];
|
|
74
|
+
}, [props.baseColor, context.baseColor, props.contrastColor, context.contrastColor]);
|
|
75
|
+
const checkboxStyle: CSSProperties = useMemo(() => {
|
|
79
76
|
let color: string;
|
|
80
|
-
if (
|
|
77
|
+
if (!!error) color = 'red';
|
|
81
78
|
else if (disabled) color = 'color-gray-4';
|
|
82
79
|
else {
|
|
83
80
|
if (useDarkestColor)
|
|
84
|
-
color = findDarkestColor(
|
|
85
|
-
else color =
|
|
81
|
+
color = findDarkestColor(baseColor, contrastColor);
|
|
82
|
+
else color = baseColor;
|
|
86
83
|
if (isHovered) color = editColor(-.2, color);
|
|
87
84
|
}
|
|
88
85
|
return {borderColor: convertColor(color)};
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let color: string;
|
|
94
|
-
if (isNotNull(error)) color = 'red';
|
|
86
|
+
}, [error, useDarkestColor, isHovered, disabled, baseColor, contrastColor]);
|
|
87
|
+
const checkStyle: CSSProperties = useMemo(() => {
|
|
88
|
+
let color: string = baseColor;
|
|
89
|
+
if (!!error) color = 'red';
|
|
95
90
|
else if (disabled) color = 'color-gray-4';
|
|
96
|
-
else if (useDarkestColor) color = findDarkestColor(
|
|
97
|
-
else color = this.getBaseColor();
|
|
91
|
+
else if (useDarkestColor) color = findDarkestColor(baseColor, contrastColor);
|
|
98
92
|
return {color: convertColor(color)};
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (isNull(label) || labelPosition !== position) return null;
|
|
109
|
-
return <label className={this.getLabelClass(position)}>{label}</label>
|
|
110
|
-
};
|
|
93
|
+
}, [error, disabled, useDarkestColor, baseColor, contrastColor]);
|
|
94
|
+
const mainColor = useMemo(() => {
|
|
95
|
+
return useDarkestColor ? findDarkestColor(baseColor, contrastColor) : baseColor;
|
|
96
|
+
}, [useDarkestColor, props.baseColor, context.baseColor, props.contrastColor, context.contrastColor]);
|
|
97
|
+
|
|
98
|
+
function renderLabel(position: 'left' | 'right'): React.ReactNode {
|
|
99
|
+
if (!label || labelPosition !== position) return null;
|
|
100
|
+
return <label className={classNames(labelClassName, position)}>{label}</label>
|
|
101
|
+
}
|
|
111
102
|
|
|
112
|
-
renderCheckbox
|
|
103
|
+
function renderCheckbox() {
|
|
113
104
|
return (
|
|
114
|
-
<div className={
|
|
115
|
-
<div className='ripple' style={{backgroundColor:
|
|
116
|
-
{
|
|
105
|
+
<div className={checkboxClassName} style={checkboxStyle}>
|
|
106
|
+
<div className='ripple' style={{backgroundColor: mainColor}}/>
|
|
107
|
+
{renderCheck()}
|
|
117
108
|
</div>
|
|
118
109
|
)
|
|
119
|
-
}
|
|
110
|
+
}
|
|
120
111
|
|
|
121
|
-
renderCheck
|
|
122
|
-
const {checkIcon} = this.props;
|
|
112
|
+
function renderCheck() {
|
|
123
113
|
if (React.isValidElement(checkIcon)) return React.cloneElement((checkIcon as React.ReactElement), {
|
|
124
|
-
className: classNames(checkIcon.props.className,
|
|
125
|
-
style: {...checkIcon.props.style, ...
|
|
114
|
+
className: classNames(checkIcon.props.className, checkClassName),
|
|
115
|
+
style: {...checkIcon.props.style, ...checkStyle}
|
|
126
116
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const {error, errorClass} = this.props;
|
|
133
|
-
return <label className={classNames('error', errorClass)}>{error}</label>;
|
|
134
|
-
};
|
|
117
|
+
let checkName: IconName = 'check';
|
|
118
|
+
if(typeof checkIcon === 'string') checkName = checkIcon as IconName;
|
|
119
|
+
else if (asRadio) checkName = 'circle';
|
|
120
|
+
return <AwesomeIcon name={checkName} className={checkClassName} style={checkStyle}/>;
|
|
121
|
+
}
|
|
135
122
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<div className={this.getContainerClass()} id={id} onClick={onCheck} onMouseEnter={onMouseEnter}
|
|
140
|
-
title={title} onMouseLeave={onMouseLeave}>
|
|
141
|
-
{this.renderLabel('left')}
|
|
142
|
-
{this.renderCheckbox()}
|
|
143
|
-
{this.renderLabel('right')}
|
|
144
|
-
{this.renderError()}
|
|
145
|
-
</div>
|
|
146
|
-
)
|
|
123
|
+
function renderError() {
|
|
124
|
+
return <label className={classNames('error', props.errorClassName)}>{error}</label>;
|
|
147
125
|
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className={className} id={id} onClick={onCheck} onMouseEnter={onMouseEnter}
|
|
129
|
+
title={title} onMouseLeave={onMouseLeave}>
|
|
130
|
+
{renderLabel('left')}
|
|
131
|
+
{renderCheckbox()}
|
|
132
|
+
{renderLabel('right')}
|
|
133
|
+
{renderError()}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
148
136
|
}
|
|
@@ -2,136 +2,214 @@
|
|
|
2
2
|
* Copyright (c) 2021. Dave van Rijn Development
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React from 'react';
|
|
6
|
-
import
|
|
7
|
-
import {ControlContext} from "../util/controlContext";
|
|
5
|
+
import React, {MouseEventHandler, useEffect, useRef, useState} from 'react';
|
|
6
|
+
import DvrdCheckbox from "./checkbox";
|
|
8
7
|
import {generateComponentId} from '../../..';
|
|
9
8
|
|
|
10
9
|
interface Props {
|
|
11
10
|
onCheck: Function;
|
|
12
|
-
onMouseEnter?:
|
|
13
|
-
onMouseLeave?:
|
|
11
|
+
onMouseEnter?: MouseEventHandler;
|
|
12
|
+
onMouseLeave?: MouseEventHandler;
|
|
14
13
|
checked: boolean;
|
|
15
|
-
disabled
|
|
16
|
-
label
|
|
17
|
-
labelPosition
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
label?: string;
|
|
16
|
+
labelPosition?: 'left' | 'right';
|
|
17
|
+
className?: string;
|
|
18
|
+
labelClassName?: string;
|
|
19
|
+
checkboxClassName?: string;
|
|
20
|
+
checkClassName?: string;
|
|
21
|
+
errorClassName?: string;
|
|
23
22
|
baseColor?: string;
|
|
24
23
|
contrastColor?: string;
|
|
25
|
-
error
|
|
24
|
+
error?: string | null;
|
|
26
25
|
id?: string;
|
|
27
|
-
checkIcon
|
|
28
|
-
unControlled
|
|
26
|
+
checkIcon?: React.ReactNode;
|
|
27
|
+
unControlled?: boolean;
|
|
29
28
|
title?: string;
|
|
30
|
-
useDarkestColor
|
|
29
|
+
useDarkestColor?: boolean;
|
|
31
30
|
ref?: any;
|
|
31
|
+
asRadio?: boolean;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
static defaultProps = {
|
|
43
|
-
disabled: false,
|
|
44
|
-
label: '',
|
|
45
|
-
labelPosition: 'right',
|
|
46
|
-
containerClass: '',
|
|
47
|
-
labelClass: '',
|
|
48
|
-
checkboxClass: '',
|
|
49
|
-
checkClass: '',
|
|
50
|
-
errorClass: '',
|
|
51
|
-
checkIcon: null,
|
|
52
|
-
error: null,
|
|
53
|
-
unControlled: false,
|
|
54
|
-
useDarkestColor: true,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
private getContainer = (): HTMLElement | null => {
|
|
58
|
-
return document.getElementById(this.id);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
private getRipple = (): HTMLElement | null => {
|
|
62
|
-
const container = this.getContainer();
|
|
63
|
-
if (container === null) return null;
|
|
64
|
-
return container.querySelector('div.ripple');
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
private id: string = generateComponentId(this.props.id);
|
|
34
|
+
export default function CheckboxController(props: Props) {
|
|
35
|
+
const {
|
|
36
|
+
disabled, unControlled, checked, onCheck, label, labelPosition, className, labelClassName, checkboxClassName,
|
|
37
|
+
checkClassName, error, checkIcon, errorClassName, title, useDarkestColor, baseColor, contrastColor, asRadio
|
|
38
|
+
} = props;
|
|
39
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
40
|
+
const [internalValue, setInternalValue] = useState(props.checked);
|
|
41
|
+
const id = useRef(generateComponentId(props.id));
|
|
68
42
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
onCheck = (evt: React.MouseEvent<Element, Event>) => {
|
|
72
|
-
const {checked, onCheck, disabled, unControlled} = this.props;
|
|
43
|
+
function _onCheck(evt: React.MouseEvent<Element, Event>) {
|
|
73
44
|
if (disabled) return;
|
|
74
45
|
if (unControlled) {
|
|
75
46
|
evt.stopPropagation();
|
|
76
47
|
evt.persist();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
const nextInternal = !internalValue;
|
|
49
|
+
setInternalValue(nextInternal);
|
|
50
|
+
onCheck(nextInternal, evt);
|
|
80
51
|
} else onCheck(!checked, evt);
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
onMouseEnter = () => {
|
|
85
|
-
if (this.props.disabled) return;
|
|
86
|
-
this.setState({isHovered: true});
|
|
87
|
-
};
|
|
52
|
+
}
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
54
|
+
function onMouseEnter(evt: React.MouseEvent) {
|
|
55
|
+
if (disabled) return;
|
|
56
|
+
setIsHovered(true);
|
|
57
|
+
props.onMouseEnter?.(evt);
|
|
58
|
+
}
|
|
93
59
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (ripple !== null) {
|
|
99
|
-
if (container.classList.contains('rippling'))
|
|
100
|
-
container.classList.remove('rippling');
|
|
101
|
-
ripple.addEventListener('transitionend', this.removeRipple);
|
|
102
|
-
container.classList.add('rippling');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
60
|
+
function onMouseLeave(evt: React.MouseEvent) {
|
|
61
|
+
setIsHovered(false);
|
|
62
|
+
props.onMouseLeave?.(evt);
|
|
63
|
+
}
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
65
|
+
function addRipple() {
|
|
66
|
+
const container = getContainer();
|
|
67
|
+
if (!container) return;
|
|
68
|
+
const ripple = getRipple();
|
|
69
|
+
if (!ripple) return;
|
|
70
|
+
container.classList.remove('rippling');
|
|
71
|
+
ripple.addEventListener('transitionend', removeRipple);
|
|
72
|
+
container.classList.add('rippling');
|
|
73
|
+
}
|
|
115
74
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
75
|
+
function removeRipple() {
|
|
76
|
+
const container = getContainer();
|
|
77
|
+
if (!container) return;
|
|
78
|
+
const ripple = getRipple();
|
|
79
|
+
if (!ripple) return;
|
|
80
|
+
ripple.removeEventListener('transitionend', removeRipple);
|
|
81
|
+
container.classList.remove('rippling');
|
|
82
|
+
}
|
|
120
83
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
84
|
+
function getContainer() {
|
|
85
|
+
return document.getElementById(id.current);
|
|
86
|
+
}
|
|
124
87
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
checked, disabled, label, labelPosition, containerClass, labelClass, checkboxClass, checkClass, checkIcon,
|
|
128
|
-
error, errorClass, unControlled, title, baseColor, contrastColor, useDarkestColor,
|
|
129
|
-
} = this.props, {isHovered, internalValue} = this.state, isChecked = unControlled ? internalValue : checked;
|
|
130
|
-
return <Checkbox onCheck={this.onCheck} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
|
|
131
|
-
checked={isChecked} disabled={disabled} isHovered={isHovered} label={label}
|
|
132
|
-
labelPosition={labelPosition} containerClass={containerClass} labelClass={labelClass}
|
|
133
|
-
checkboxClass={checkboxClass} checkClass={checkClass} id={this.id} checkIcon={checkIcon}
|
|
134
|
-
error={error} errorClass={errorClass} title={title} baseColor={baseColor}
|
|
135
|
-
contrastColor={contrastColor} useDarkestColor={useDarkestColor}/>
|
|
88
|
+
function getRipple() {
|
|
89
|
+
return getContainer()?.querySelector('div.ripple') ?? null;
|
|
136
90
|
}
|
|
137
|
-
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (unControlled && internalValue) addRipple();
|
|
94
|
+
else if (!unControlled && checked) addRipple();
|
|
95
|
+
return function () {
|
|
96
|
+
removeRipple();
|
|
97
|
+
}
|
|
98
|
+
}, [internalValue, checked]);
|
|
99
|
+
|
|
100
|
+
const isChecked = unControlled ? internalValue : checked
|
|
101
|
+
return (
|
|
102
|
+
<DvrdCheckbox onCheck={_onCheck} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} checked={isChecked}
|
|
103
|
+
disabled={disabled ?? false} isHovered={isHovered} label={label}
|
|
104
|
+
labelPosition={labelPosition ?? 'right'} className={className} labelClassName={labelClassName}
|
|
105
|
+
checkboxClassName={checkboxClassName} checkClassName={checkClassName} id={id.current}
|
|
106
|
+
checkIcon={checkIcon} error={error ?? null} errorClassName={errorClassName} title={title}
|
|
107
|
+
baseColor={baseColor} contrastColor={contrastColor} useDarkestColor={useDarkestColor ?? true}
|
|
108
|
+
asRadio={asRadio}/>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// interface CheckboxState {
|
|
113
|
+
// isHovered: boolean;
|
|
114
|
+
// internalValue: boolean;
|
|
115
|
+
// }
|
|
116
|
+
//
|
|
117
|
+
// class _CheckboxController extends React.Component<Props, CheckboxState> {
|
|
118
|
+
// static contextType = ControlContext;
|
|
119
|
+
//
|
|
120
|
+
// static defaultProps = {
|
|
121
|
+
// disabled: false,
|
|
122
|
+
// label: '',
|
|
123
|
+
// labelPosition: 'right',
|
|
124
|
+
// containerClass: '',
|
|
125
|
+
// labelClass: '',
|
|
126
|
+
// checkboxClass: '',
|
|
127
|
+
// checkClass: '',
|
|
128
|
+
// errorClass: '',
|
|
129
|
+
// checkIcon: null,
|
|
130
|
+
// error: null,
|
|
131
|
+
// unControlled: false,
|
|
132
|
+
// useDarkestColor: true,
|
|
133
|
+
// };
|
|
134
|
+
//
|
|
135
|
+
// private getContainer = (): HTMLElement | null => {
|
|
136
|
+
// return document.getElementById(this.id);
|
|
137
|
+
// };
|
|
138
|
+
//
|
|
139
|
+
// private getRipple = (): HTMLElement | null => {
|
|
140
|
+
// const container = this.getContainer();
|
|
141
|
+
// if (container === null) return null;
|
|
142
|
+
// return container.querySelector('div.ripple');
|
|
143
|
+
// };
|
|
144
|
+
//
|
|
145
|
+
// private id: string = generateComponentId(this.props.id);
|
|
146
|
+
//
|
|
147
|
+
// state = {isHovered: false, internalValue: this.props.checked};
|
|
148
|
+
//
|
|
149
|
+
// onCheck = (evt: React.MouseEvent<Element, Event>) => {
|
|
150
|
+
// const {checked, onCheck, disabled, unControlled} = this.props;
|
|
151
|
+
// if (disabled) return;
|
|
152
|
+
// if (unControlled) {
|
|
153
|
+
// evt.stopPropagation();
|
|
154
|
+
// evt.persist();
|
|
155
|
+
// this.setState({internalValue: !this.state.internalValue}, () => {
|
|
156
|
+
// onCheck(this.state.internalValue, evt);
|
|
157
|
+
// });
|
|
158
|
+
// } else onCheck(!checked, evt);
|
|
159
|
+
// this.addRipple();
|
|
160
|
+
// };
|
|
161
|
+
//
|
|
162
|
+
// onMouseEnter = () => {
|
|
163
|
+
// if (this.props.disabled) return;
|
|
164
|
+
// this.setState({isHovered: true});
|
|
165
|
+
// };
|
|
166
|
+
//
|
|
167
|
+
// onMouseLeave = () => {
|
|
168
|
+
// if (this.props.disabled) return;
|
|
169
|
+
// this.setState({isHovered: false});
|
|
170
|
+
// };
|
|
171
|
+
//
|
|
172
|
+
// addRipple = () => {
|
|
173
|
+
// const container = this.getContainer();
|
|
174
|
+
// if (container !== null) {
|
|
175
|
+
// const ripple = this.getRipple();
|
|
176
|
+
// if (ripple !== null) {
|
|
177
|
+
// if (container.classList.contains('rippling'))
|
|
178
|
+
// container.classList.remove('rippling');
|
|
179
|
+
// ripple.addEventListener('transitionend', this.removeRipple);
|
|
180
|
+
// container.classList.add('rippling');
|
|
181
|
+
// }
|
|
182
|
+
// }
|
|
183
|
+
// };
|
|
184
|
+
//
|
|
185
|
+
// removeRipple = () => {
|
|
186
|
+
// const ripple = this.getRipple();
|
|
187
|
+
// const container = this.getContainer();
|
|
188
|
+
// if (ripple !== null && container !== null) {
|
|
189
|
+
// ripple.removeEventListener('transitionend', this.removeRipple);
|
|
190
|
+
// container.classList.remove('rippling');
|
|
191
|
+
// }
|
|
192
|
+
// };
|
|
193
|
+
//
|
|
194
|
+
// componentDidUpdate = (prevProps: Props, prevState: CheckboxState) => {
|
|
195
|
+
// if (prevState.internalValue !== this.state.internalValue && this.state.internalValue) this.addRipple();
|
|
196
|
+
// else if (prevProps.checked !== this.props.checked && this.props.checked) this.addRipple();
|
|
197
|
+
// };
|
|
198
|
+
//
|
|
199
|
+
// componentWillUnmount = () => {
|
|
200
|
+
// this.removeRipple();
|
|
201
|
+
// };
|
|
202
|
+
//
|
|
203
|
+
// render = () => {
|
|
204
|
+
// const {
|
|
205
|
+
// checked, disabled, label, labelPosition, containerClass, labelClass, checkboxClass, checkClass, checkIcon,
|
|
206
|
+
// error, errorClass, unControlled, title, baseColor, contrastColor, useDarkestColor,
|
|
207
|
+
// } = this.props, {isHovered, internalValue} = this.state, isChecked = unControlled ? internalValue : checked;
|
|
208
|
+
// return <Checkbox onCheck={this.onCheck} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
|
|
209
|
+
// checked={isChecked} disabled={disabled} isHovered={isHovered} label={label}
|
|
210
|
+
// labelPosition={labelPosition} containerClass={containerClass} labelClass={labelClass}
|
|
211
|
+
// checkboxClass={checkboxClass} checkClass={checkClass} id={this.id} checkIcon={checkIcon}
|
|
212
|
+
// error={error} errorClass={errorClass} title={title} baseColor={baseColor}
|
|
213
|
+
// contrastColor={contrastColor} useDarkestColor={useDarkestColor}/>
|
|
214
|
+
// }
|
|
215
|
+
// }
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
grid-template-columns: 22px auto;
|
|
10
10
|
grid-column-gap: .5rem;
|
|
11
11
|
align-items: center;
|
|
12
|
-
//padding: .2rem;
|
|
13
12
|
cursor: pointer;
|
|
14
13
|
position: relative;
|
|
15
14
|
margin-bottom: 0;
|
|
@@ -63,6 +62,15 @@
|
|
|
63
62
|
visibility: hidden;
|
|
64
63
|
pointer-events: none;
|
|
65
64
|
}
|
|
65
|
+
|
|
66
|
+
&.as-radio {
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
|
|
69
|
+
.checkbox-check {
|
|
70
|
+
@include centerXY;
|
|
71
|
+
font-size: 10px;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
.error {
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024. Dave van Rijn Development
|
|
3
|
+
*/
|
|
4
|
+
import './style/dvrdSelect.scss';
|
|
5
|
+
|
|
6
|
+
import React, {MouseEventHandler, ReactElement, useEffect, useMemo, useRef, useState} from 'react';
|
|
7
|
+
import classNames from 'classnames';
|
|
8
|
+
import {ChangeFunction, ErrorType, SelectItemShape} from '../util/interfaces';
|
|
9
|
+
import {hasHover, stopPropagation} from '../util/controlUtil';
|
|
10
|
+
import AwesomeIcon from '../icon/awesomeIcon';
|
|
11
|
+
import delay from 'lodash.delay';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
onChange: (selected: Array<string | number>) => MouseEventHandler;
|
|
15
|
+
onChangeSearch: ChangeFunction;
|
|
16
|
+
selected: Array<string | number>;
|
|
17
|
+
items: SelectItemShape[];
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
error?: ErrorType;
|
|
20
|
+
label?: string;
|
|
21
|
+
className?: string;
|
|
22
|
+
labelClassName?: string;
|
|
23
|
+
valueClassName?: string;
|
|
24
|
+
arrowClassName?: string;
|
|
25
|
+
errorClassName?: string;
|
|
26
|
+
itemContainerClassName?: string;
|
|
27
|
+
itemClassName?: string;
|
|
28
|
+
itemsPosition: 'top' | 'bottom';
|
|
29
|
+
selectOnly: boolean;
|
|
30
|
+
searchValue: string;
|
|
31
|
+
optionsContainerHeight: number | string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// interface State {
|
|
35
|
+
// open: boolean;
|
|
36
|
+
// searchFocused: boolean;
|
|
37
|
+
// }
|
|
38
|
+
|
|
39
|
+
const transitionDuration = 200; // ms
|
|
40
|
+
|
|
41
|
+
export default function DvrdMultiSelect(props: Props) {
|
|
42
|
+
const {
|
|
43
|
+
error, disabled, selected, onChange, onChangeSearch, items, optionsContainerHeight, label, labelClassName,
|
|
44
|
+
selectOnly, valueClassName, searchValue, arrowClassName, itemClassName, itemsPosition, itemContainerClassName,
|
|
45
|
+
errorClassName, className
|
|
46
|
+
} = props;
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const [searchFocused, setSearchFocused] = useState(false);
|
|
49
|
+
const container = useRef<HTMLDivElement>(null);
|
|
50
|
+
const hasError = useMemo(() => !!error, [error]);
|
|
51
|
+
const itemsContainerStyle = useMemo(() => {
|
|
52
|
+
let maxHeight: string | number = optionsContainerHeight;
|
|
53
|
+
if (typeof maxHeight === 'number') maxHeight = `${maxHeight}px`;
|
|
54
|
+
return {maxHeight: maxHeight};
|
|
55
|
+
}, [optionsContainerHeight])
|
|
56
|
+
|
|
57
|
+
function onClickElement(evt: React.MouseEvent) {
|
|
58
|
+
stopPropagation(evt);
|
|
59
|
+
if (disabled) return;
|
|
60
|
+
setOpen(!open);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onClickItem(_value: string | number) {
|
|
64
|
+
return function (evt: React.MouseEvent) {
|
|
65
|
+
stopPropagation(evt);
|
|
66
|
+
if (selected.includes(_value))
|
|
67
|
+
onChange(selected.filter((value: string | number) => value !== _value))(evt);
|
|
68
|
+
else onChange(selected.concat(_value))(evt);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onFocusInput() {
|
|
73
|
+
setSearchFocused(true);
|
|
74
|
+
setOpen(true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onBlurInput() {
|
|
78
|
+
delay(() => {
|
|
79
|
+
setSearchFocused(false);
|
|
80
|
+
onChangeSearch({target: {value: ''}});
|
|
81
|
+
}, transitionDuration);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getLongestValue(): string {
|
|
85
|
+
let longest: string = '';
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
const stringLabel = item.label.toString();
|
|
88
|
+
if (stringLabel.length > longest.length)
|
|
89
|
+
longest = stringLabel;
|
|
90
|
+
}
|
|
91
|
+
return longest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getValueLabel(): string {
|
|
95
|
+
let valueLabel = '';
|
|
96
|
+
let extra = 0;
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
if (selected.includes(item.value)) {
|
|
99
|
+
if (valueLabel.length) extra++;
|
|
100
|
+
else valueLabel = getItemLabel(item);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!valueLabel.length) return '';
|
|
104
|
+
if (extra) valueLabel += ` +${extra}`
|
|
105
|
+
return valueLabel;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getItemLabel(item: SelectItemShape): string {
|
|
109
|
+
const {label} = item;
|
|
110
|
+
if (['string', 'number'].includes(typeof label)) return label.toString();
|
|
111
|
+
else if (!!item.valueLabel) return item.valueLabel;
|
|
112
|
+
return item.value.toString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderLabel() {
|
|
116
|
+
if (!label) return null;
|
|
117
|
+
return <label className={classNames('dvrd-select-label', labelClassName)}
|
|
118
|
+
onClick={onClickElement}>{label}</label>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderValue() {
|
|
122
|
+
const valueLabel = getValueLabel();
|
|
123
|
+
if (selectOnly)
|
|
124
|
+
return (
|
|
125
|
+
<div style={{display: 'flex', flexDirection: 'column'}}>
|
|
126
|
+
<label className={classNames('dvrd-select-value', valueClassName)}
|
|
127
|
+
onClick={onClickElement}>{valueLabel}</label>
|
|
128
|
+
<label style={{height: 0, visibility: 'hidden'}}>{getLongestValue()}</label>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
const value = searchFocused ? searchValue : valueLabel;
|
|
132
|
+
return <input className='dvrd-select-search' value={value} onChange={onChangeSearch} disabled={disabled}
|
|
133
|
+
onFocus={onFocusInput} onBlur={onBlurInput} onClick={stopPropagation}/>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderArrow() {
|
|
137
|
+
return <AwesomeIcon name='chevron-down' onClick={onClickElement}
|
|
138
|
+
className={classNames('dvrd-select-arrow', open && 'open', arrowClassName)}/>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderItems() {
|
|
142
|
+
const _items = items.filter((item: SelectItemShape) => {
|
|
143
|
+
if (item.selectable === false) return false;
|
|
144
|
+
if (typeof item.label === 'string') return item.label.length > 0;
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
className={classNames('dvrd-select-items', (open && !disabled) && 'open', itemsPosition,
|
|
150
|
+
itemContainerClassName)} style={itemsContainerStyle}>
|
|
151
|
+
{_items.map(renderItem)}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderItem(item: SelectItemShape, index: number) {
|
|
157
|
+
const {value, selectableItemLabel} = item;
|
|
158
|
+
const isSelected = selected.includes(value);
|
|
159
|
+
const label = selectableItemLabel ?? item.label;
|
|
160
|
+
if (['number', 'string'].includes(typeof label)) return (
|
|
161
|
+
<label key={index} className={classNames('dvrd-select-item', isSelected && 'selected', itemClassName)}
|
|
162
|
+
onClick={onClickItem(value)}>{label}</label>
|
|
163
|
+
);
|
|
164
|
+
const labelElement: ReactElement = label as ReactElement;
|
|
165
|
+
const onClick = (evt: React.MouseEvent) => {
|
|
166
|
+
onClickItem(value)(evt);
|
|
167
|
+
labelElement.props.onClick?.(evt);
|
|
168
|
+
};
|
|
169
|
+
const className = classNames(labelElement.props.className, 'dvrd-select-item', isSelected && 'selected', itemClassName);
|
|
170
|
+
return React.cloneElement(labelElement, {...labelElement.props, onClick, className, key: index});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderError() {
|
|
174
|
+
if (!hasError) return null;
|
|
175
|
+
return <label className={classNames('dvrd-select-error', errorClassName)}>{error}</label>
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addClickListener() {
|
|
179
|
+
document.addEventListener('click', clickListener);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function removeClickListener() {
|
|
183
|
+
document.removeEventListener('click', clickListener);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clickListener() {
|
|
187
|
+
if (!hasHover(container.current)) setOpen(false);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (open) addClickListener();
|
|
192
|
+
else removeClickListener();
|
|
193
|
+
return function () {
|
|
194
|
+
removeClickListener();
|
|
195
|
+
}
|
|
196
|
+
}, [open]);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className={classNames('dvrd-select-container', 'multi', disabled && 'disabled', open && 'open',
|
|
200
|
+
hasError && 'error', className)} ref={container} onClick={onClickElement}>
|
|
201
|
+
{renderLabel()}
|
|
202
|
+
<div className={classNames('content-container', !selectOnly && 'search')}>
|
|
203
|
+
{renderValue()}
|
|
204
|
+
{renderArrow()}
|
|
205
|
+
</div>
|
|
206
|
+
{renderItems()}
|
|
207
|
+
{renderError()}
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// class DvrdSelect extends PureComponent<Props, State> {
|
|
213
|
+
// private container: HTMLDivElement;
|
|
214
|
+
// state: State = {
|
|
215
|
+
// open: false,
|
|
216
|
+
// searchFocused: false,
|
|
217
|
+
// };
|
|
218
|
+
//
|
|
219
|
+
// hasError = (): boolean => this.props.error !== undefined && this.props.error !== null;
|
|
220
|
+
//
|
|
221
|
+
// onClickElement = (evt: React.MouseEvent) => {
|
|
222
|
+
// evt.stopPropagation();
|
|
223
|
+
// if (!this.props.disabled)
|
|
224
|
+
// this.setState({open: !this.state.open});
|
|
225
|
+
// };
|
|
226
|
+
//
|
|
227
|
+
// onClickItem = (value: string | number) => (evt: React.MouseEvent) => {
|
|
228
|
+
// evt.stopPropagation();
|
|
229
|
+
// this.props.onChange(value)(evt);
|
|
230
|
+
// this.setState({open: false});
|
|
231
|
+
// };
|
|
232
|
+
//
|
|
233
|
+
// onFocusInput = () => {
|
|
234
|
+
// this.setState({searchFocused: true, open: true});
|
|
235
|
+
// };
|
|
236
|
+
//
|
|
237
|
+
// onBlurInput = () => {
|
|
238
|
+
// window.setTimeout(() => {
|
|
239
|
+
// this.setState({searchFocused: false});
|
|
240
|
+
// this.props.onChangeSearch({target: {value: ''}});
|
|
241
|
+
// }, 200);
|
|
242
|
+
// };
|
|
243
|
+
//
|
|
244
|
+
// getLongestValue = (): string => {
|
|
245
|
+
// const {items} = this.props;
|
|
246
|
+
// let longest: string = '';
|
|
247
|
+
// for (const item of items) {
|
|
248
|
+
// if (item.label.toString().length > longest.length) {
|
|
249
|
+
// longest = item.label.toString();
|
|
250
|
+
// }
|
|
251
|
+
// }
|
|
252
|
+
// return longest;
|
|
253
|
+
// };
|
|
254
|
+
//
|
|
255
|
+
// getValueLabel = (): string | number => {
|
|
256
|
+
// const {value, items} = this.props;
|
|
257
|
+
// for (const item of items) {
|
|
258
|
+
// if (item.value === value) return this.getItemLabel(item);
|
|
259
|
+
// }
|
|
260
|
+
// return '';
|
|
261
|
+
// }
|
|
262
|
+
//
|
|
263
|
+
// getItemLabel = (item: SelectItemShape): string | number => {
|
|
264
|
+
// const {label} = item;
|
|
265
|
+
// if (['string', 'number'].includes(typeof label)) return label as string | number;
|
|
266
|
+
// else if (!!item.valueLabel) return item.valueLabel;
|
|
267
|
+
// return item.value;
|
|
268
|
+
// }
|
|
269
|
+
//
|
|
270
|
+
// getItemsContainerStyle = (): CSSProperties => {
|
|
271
|
+
// let {optionsContainerHeight} = this.props;
|
|
272
|
+
// if (typeof optionsContainerHeight === 'number') optionsContainerHeight = `${optionsContainerHeight}px`;
|
|
273
|
+
// return {maxHeight: optionsContainerHeight};
|
|
274
|
+
// }
|
|
275
|
+
//
|
|
276
|
+
// renderLabel = () => {
|
|
277
|
+
// const {labelClassName, label} = this.props;
|
|
278
|
+
// if (!label) return null;
|
|
279
|
+
// return <label className={classNames('dvrd-select-label', labelClassName)}
|
|
280
|
+
// onClick={this.onClickElement}>{label}</label>
|
|
281
|
+
// };
|
|
282
|
+
//
|
|
283
|
+
// renderValue = () => {
|
|
284
|
+
// const {valueClassName, selectOnly, searchValue, onChangeSearch, disabled} = this.props,
|
|
285
|
+
// {searchFocused} = this.state, valueLabel = this.getValueLabel();
|
|
286
|
+
// if (selectOnly)
|
|
287
|
+
// return (
|
|
288
|
+
// <div style={{display: 'flex', flexDirection: 'column'}}>
|
|
289
|
+
// <label className={classNames('dvrd-select-value', valueClassName)}
|
|
290
|
+
// onClick={this.onClickElement}>{valueLabel}</label>
|
|
291
|
+
// <label style={{height: 0, visibility: 'hidden'}}>{this.getLongestValue()}</label>
|
|
292
|
+
// </div>
|
|
293
|
+
// );
|
|
294
|
+
// const value = searchFocused ? searchValue : valueLabel;
|
|
295
|
+
// return <input className='dvrd-select-search' value={value} onChange={onChangeSearch} disabled={disabled}
|
|
296
|
+
// onFocus={this.onFocusInput} onBlur={this.onBlurInput} onClick={stopPropagation}/>
|
|
297
|
+
// };
|
|
298
|
+
//
|
|
299
|
+
// renderArrow = () => {
|
|
300
|
+
// const {arrowClassName} = this.props, {open} = this.state;
|
|
301
|
+
// return <AwesomeIcon name='chevron-down' onClick={this.onClickElement}
|
|
302
|
+
// className={classNames('dvrd-select-arrow', open && 'open', arrowClassName)}/>;
|
|
303
|
+
// };
|
|
304
|
+
//
|
|
305
|
+
// renderItems = () => {
|
|
306
|
+
// const {disabled, itemContainerClassName, itemsPosition} = this.props, {open} = this.state;
|
|
307
|
+
// const items = this.props.items.filter((item: SelectItemShape) => {
|
|
308
|
+
// if (item.selectable === false) return false;
|
|
309
|
+
// if (typeof item.label === 'string') return item.label.length > 0;
|
|
310
|
+
// return true;
|
|
311
|
+
// })
|
|
312
|
+
// return (
|
|
313
|
+
// <div
|
|
314
|
+
// className={classNames('dvrd-select-items', (open && !disabled) && 'open', itemsPosition,
|
|
315
|
+
// itemContainerClassName)} style={this.getItemsContainerStyle()}>
|
|
316
|
+
// {items.map(this.renderItem)}
|
|
317
|
+
// </div>
|
|
318
|
+
// )
|
|
319
|
+
// };
|
|
320
|
+
//
|
|
321
|
+
// renderItem = (item: SelectItemShape, index: number) => {
|
|
322
|
+
// const {value, selectableItemLabel} = item;
|
|
323
|
+
// const label = selectableItemLabel ?? item.label;
|
|
324
|
+
// const {itemClassName} = this.props;
|
|
325
|
+
// if (['number', 'string'].includes(typeof label)) return (
|
|
326
|
+
// <label key={index} className={classNames('dvrd-select-item', itemClassName)}
|
|
327
|
+
// onClick={this.onClickItem(value)}>{label}</label>
|
|
328
|
+
// );
|
|
329
|
+
// const labelElement: ReactElement = label as ReactElement;
|
|
330
|
+
// const onClick = (evt: React.MouseEvent) => {
|
|
331
|
+
// this.onClickItem(value)(evt);
|
|
332
|
+
// labelElement.props.onClick?.(evt);
|
|
333
|
+
// };
|
|
334
|
+
// const className = classNames(labelElement.props.className, 'dvrd-select-item', itemClassName);
|
|
335
|
+
// return React.cloneElement(labelElement, Object.assign({}, labelElement.props, {
|
|
336
|
+
// onClick,
|
|
337
|
+
// className,
|
|
338
|
+
// key: index
|
|
339
|
+
// }));
|
|
340
|
+
// }
|
|
341
|
+
//
|
|
342
|
+
// renderError = () => {
|
|
343
|
+
// if (!this.hasError()) return null;
|
|
344
|
+
// const {error, errorClassName} = this.props;
|
|
345
|
+
// return <label className={classNames('dvrd-select-error', errorClassName)}>{error}</label>
|
|
346
|
+
// };
|
|
347
|
+
//
|
|
348
|
+
// addClickListener = () => {
|
|
349
|
+
// document.addEventListener('click', this.clickListener);
|
|
350
|
+
// };
|
|
351
|
+
//
|
|
352
|
+
// removeClickListener = () => {
|
|
353
|
+
// document.removeEventListener('click', this.clickListener);
|
|
354
|
+
// }
|
|
355
|
+
//
|
|
356
|
+
// clickListener = () => {
|
|
357
|
+
// if (!hasHover(this.container)) this.setState({open: false});
|
|
358
|
+
// };
|
|
359
|
+
//
|
|
360
|
+
// componentDidUpdate = (props: Props, prevState: State) => {
|
|
361
|
+
// const {open} = this.state, prevOpen = prevState.open;
|
|
362
|
+
// if (open && !prevOpen) this.addClickListener();
|
|
363
|
+
// else if (!open && prevOpen) this.removeClickListener();
|
|
364
|
+
// };
|
|
365
|
+
//
|
|
366
|
+
// componentWillUnmount = () => {
|
|
367
|
+
// this.removeClickListener();
|
|
368
|
+
// };
|
|
369
|
+
//
|
|
370
|
+
// render = () => {
|
|
371
|
+
// const {className, disabled, selectOnly} = this.props, {open} = this.state;
|
|
372
|
+
// return (
|
|
373
|
+
// <div
|
|
374
|
+
// className={classNames('dvrd-select-container', disabled && 'disabled', open && 'open',
|
|
375
|
+
// this.hasError() && 'error', className)}
|
|
376
|
+
// ref={(ref: HTMLDivElement) => {
|
|
377
|
+
// this.container = ref
|
|
378
|
+
// }} onClick={this.onClickElement}>
|
|
379
|
+
// {this.renderLabel()}
|
|
380
|
+
// <div className={classNames('content-container', !selectOnly && 'search')}>
|
|
381
|
+
// {this.renderValue()}
|
|
382
|
+
// {this.renderArrow()}
|
|
383
|
+
// </div>
|
|
384
|
+
// {this.renderItems()}
|
|
385
|
+
// {this.renderError()}
|
|
386
|
+
// </div>
|
|
387
|
+
// )
|
|
388
|
+
// };
|
|
389
|
+
// };
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* Copyright (c) 2021. Dave van Rijn Development
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
6
6
|
import DvrdSelect from "./dvrdSelect";
|
|
7
7
|
import {ChangeFunction, ErrorType, SelectItemShape} from "../util/interfaces";
|
|
8
8
|
import {stringContains} from '../util/controlUtil';
|
|
9
|
+
import DvrdMultiSelect from "./dvrdMultiSelect";
|
|
10
|
+
|
|
11
|
+
type ValueType = string | number | Array<string | number>;
|
|
9
12
|
|
|
10
13
|
interface Props {
|
|
11
|
-
onChange: ChangeFunction
|
|
12
|
-
value: string | number
|
|
14
|
+
onChange: ChangeFunction<ValueType>;
|
|
15
|
+
value: string | number | Array<string | number>;
|
|
13
16
|
items: SelectItemShape[];
|
|
14
17
|
disabled?: boolean;
|
|
15
18
|
label?: string;
|
|
@@ -21,66 +24,124 @@ interface Props {
|
|
|
21
24
|
errorClassName?: string;
|
|
22
25
|
itemContainerClassName?: string;
|
|
23
26
|
itemClassName?: string;
|
|
24
|
-
itemsPosition
|
|
25
|
-
selectOnly
|
|
26
|
-
optionsContainerHeight
|
|
27
|
-
unControlled?:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
interface State {
|
|
31
|
-
searchValue: string;
|
|
32
|
-
value: string | number;
|
|
27
|
+
itemsPosition?: 'top' | 'bottom';
|
|
28
|
+
selectOnly?: false;
|
|
29
|
+
optionsContainerHeight?: number | string;
|
|
30
|
+
unControlled?: true;
|
|
31
|
+
multi?: true;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// interface State {
|
|
35
|
+
// searchValue: string;
|
|
36
|
+
// value: string | number | Array<string | number>;
|
|
37
|
+
// }
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
39
|
+
export default function DvrdSelectController(props: Props) {
|
|
40
|
+
const {
|
|
41
|
+
value, multi, unControlled, onChange, items, error, className, labelClassName, valueClassName,
|
|
42
|
+
itemContainerClassName, arrowClassName, errorClassName, itemClassName, label, disabled
|
|
43
|
+
} = props;
|
|
44
|
+
const [search, setSearch] = useState('');
|
|
45
|
+
const [internalValue, setInternalValue] = useState<ValueType>('');
|
|
46
|
+
const _items = useMemo(() => {
|
|
47
|
+
if (!search) return items;
|
|
48
|
+
return items.filter((item) => stringContains(item.label.toString(), search));
|
|
49
|
+
}, [items, search]);
|
|
50
|
+
const _value = useMemo(() => unControlled ? internalValue : value,
|
|
51
|
+
[unControlled, internalValue, value]);
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.props.onChange(value);
|
|
51
|
-
this.setState({value});
|
|
52
|
-
};
|
|
53
|
+
function _onChange(value: ValueType) {
|
|
54
|
+
return function () {
|
|
55
|
+
onChange(value);
|
|
56
|
+
setInternalValue(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
53
59
|
|
|
54
|
-
onChangeSearch
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
60
|
+
function onChangeSearch(evt: React.ChangeEvent<HTMLInputElement>) {
|
|
61
|
+
setSearch(evt.target.value);
|
|
62
|
+
}
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (multi && !Array.isArray(value)) throw new TypeError('Value must be an array in multi mode');
|
|
66
|
+
else if (!multi && Array.isArray(value)) throw new TypeError('Value must be a string or number in single mode');
|
|
67
|
+
if (!unControlled && value !== internalValue) setInternalValue(value);
|
|
68
|
+
}, [value, multi, unControlled]);
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
const itemsPosition = props.itemsPosition ?? 'bottom';
|
|
71
|
+
const optionsContainerHeight = props.optionsContainerHeight ?? '15rem';
|
|
72
|
+
const selectOnly = props.selectOnly !== false;
|
|
73
|
+
if (multi) return (
|
|
74
|
+
<DvrdMultiSelect onChange={_onChange} onChangeSearch={onChangeSearch}
|
|
75
|
+
selected={_value as Array<string | number>} items={_items} itemsPosition={itemsPosition}
|
|
76
|
+
selectOnly={selectOnly} searchValue={search} optionsContainerHeight={optionsContainerHeight}
|
|
77
|
+
className={className} error={error} label={label} arrowClassName={arrowClassName}
|
|
78
|
+
errorClassName={errorClassName} itemClassName={itemClassName}
|
|
79
|
+
itemContainerClassName={itemContainerClassName} valueClassName={valueClassName}
|
|
80
|
+
labelClassName={labelClassName} disabled={disabled}/>
|
|
81
|
+
);
|
|
82
|
+
return (
|
|
83
|
+
<DvrdSelect onChange={_onChange} onChangeSearch={onChangeSearch} value={_value as number | string}
|
|
84
|
+
items={_items} itemsPosition={itemsPosition} selectOnly={selectOnly} searchValue={search}
|
|
85
|
+
optionsContainerHeight={optionsContainerHeight} className={className} error={error} label={label}
|
|
86
|
+
arrowClassName={arrowClassName} errorClassName={errorClassName} itemClassName={itemClassName}
|
|
87
|
+
itemContainerClassName={itemContainerClassName} valueClassName={valueClassName}
|
|
88
|
+
labelClassName={labelClassName} disabled={disabled}/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
92
|
+
// class _DvrdSelectController extends PureComponent<Props, State> {
|
|
93
|
+
// private select: DvrdSelect;
|
|
94
|
+
//
|
|
95
|
+
// static defaultProps = {
|
|
96
|
+
// itemsPosition: 'bottom',
|
|
97
|
+
// selectOnly: true,
|
|
98
|
+
// optionsContainerHeight: '15rem',
|
|
99
|
+
// };
|
|
100
|
+
//
|
|
101
|
+
// state: State = {
|
|
102
|
+
// searchValue: '',
|
|
103
|
+
// value: this.props.value,
|
|
104
|
+
// };
|
|
105
|
+
//
|
|
106
|
+
// onChange = (value: string | number) => () => {
|
|
107
|
+
// this.props.onChange(value);
|
|
108
|
+
// this.setState({value});
|
|
109
|
+
// };
|
|
110
|
+
//
|
|
111
|
+
// onChangeSearch = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
112
|
+
// const {value} = evt.target;
|
|
113
|
+
// this.setState({searchValue: value});
|
|
114
|
+
// };
|
|
115
|
+
//
|
|
116
|
+
// onOpen = (evt: React.MouseEvent) => {
|
|
117
|
+
// if (this.select) this.select.onClickElement(evt);
|
|
118
|
+
// };
|
|
119
|
+
//
|
|
120
|
+
// getItems = () => {
|
|
121
|
+
// const items = this.props.items.slice(), {searchValue} = this.state;
|
|
122
|
+
// if (!searchValue) return items;
|
|
123
|
+
// return items.filter((item) => stringContains(item.label.toString(), searchValue))
|
|
124
|
+
// }
|
|
125
|
+
//
|
|
126
|
+
// render = () => {
|
|
127
|
+
// const {
|
|
128
|
+
// arrowClassName, label, error, className, labelClassName, valueClassName, itemContainerClassName,
|
|
129
|
+
// itemClassName, errorClassName, disabled, itemsPosition, selectOnly, optionsContainerHeight, unControlled,
|
|
130
|
+
// multi
|
|
131
|
+
// } = this.props, {searchValue} = this.state;
|
|
132
|
+
// const value = !!unControlled ? this.state.value : this.props.value;
|
|
133
|
+
// if (!multi && Array.isArray(value)) throw new TypeError('Value cannot be an array in single mode');
|
|
134
|
+
// else if (multi && !Array.isArray(value)) throw new TypeError('Value must be an array in multi mode');
|
|
135
|
+
// return (
|
|
136
|
+
// <DvrdSelect onChange={this.onChange} value={value as string | number} items={this.getItems()}
|
|
137
|
+
// disabled={disabled}
|
|
138
|
+
// errorClassName={errorClassName} itemClassName={itemClassName}
|
|
139
|
+
// itemContainerClassName={itemContainerClassName} valueClassName={valueClassName}
|
|
140
|
+
// labelClassName={labelClassName} className={className} error={error} label={label}
|
|
141
|
+
// arrowClassName={arrowClassName} itemsPosition={itemsPosition} ref={(ref: DvrdSelect) => {
|
|
142
|
+
// this.select = ref
|
|
143
|
+
// }} searchValue={searchValue} onChangeSearch={this.onChangeSearch} selectOnly={selectOnly}
|
|
144
|
+
// optionsContainerHeight={optionsContainerHeight}/>
|
|
145
|
+
// );
|
|
146
|
+
// }
|
|
147
|
+
// };
|