@dvrd/dvr-controls 0.0.27 → 0.0.31
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 +6 -71
- package/src/js/sidebarMenu/sidebarMenu.tsx +72 -136
- package/src/js/sidebarMenu/style/sidebarMenu.scss +33 -7
- package/src/js/snackbar/snackbarController.tsx +20 -3
- package/src/js/textField/dvrdInput.tsx +8 -6
- package/src/js/textField/dvrdInputController.tsx +47 -51
- package/src/js/textField/dvrdNumberInput.tsx +1 -0
- package/src/js/util/miscUtil.ts +5 -8
- package/src/js/util/requestUtil.ts +22 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvrd/dvr-controls",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
4
4
|
"description": "Custom web controls",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"scripts": {},
|
|
@@ -10,98 +10,33 @@
|
|
|
10
10
|
},
|
|
11
11
|
"author": "Dave van Rijn",
|
|
12
12
|
"license": "ISC",
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@sentry/browser": "^6.2.1",
|
|
15
|
-
"@types/js-cookie": "^2.2.6",
|
|
16
|
-
"@types/lodash": "^4.14.168",
|
|
17
|
-
"@types/node": "14.14.32",
|
|
18
|
-
"@types/react": "~17.0.2",
|
|
19
|
-
"@types/react-dom": "17.0.1",
|
|
20
|
-
"@types/react-router-dom": "~5.1.6",
|
|
21
|
-
"@types/uuid": "^8.3.0",
|
|
22
|
-
"@types/react-color": "^3.0.4",
|
|
23
|
-
"@types/dompurify": "2.2.1",
|
|
24
|
-
"awesome-typescript-loader": "^5.2.1",
|
|
25
|
-
"classnames": "^2.2.6",
|
|
26
|
-
"compression-webpack-plugin": "^7.1.2",
|
|
27
|
-
"cross-env": "^7.0.3",
|
|
28
|
-
"css-loader": "^5.1.1",
|
|
29
|
-
"cssnano": "^4.1.10",
|
|
30
|
-
"cssnano-preset-advanced": "^4.0.7",
|
|
31
|
-
"file-loader": "^6.2.0",
|
|
32
|
-
"html-webpack-plugin": "^5.3.0",
|
|
33
|
-
"js-cookie": "^2.2.1",
|
|
34
|
-
"lodash": "^4.17.21",
|
|
35
|
-
"mini-css-extract-plugin": "^1.3.9",
|
|
36
|
-
"moment": "^2.29.1",
|
|
37
|
-
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
|
38
|
-
"prerender-spa-plugin": "^3.4.0",
|
|
39
|
-
"query-string": "^6.14.1",
|
|
40
|
-
"react": "^17.0.1",
|
|
41
|
-
"react-color": "^2.19.3",
|
|
42
|
-
"react-dom": "^17.0.1",
|
|
43
|
-
"react-router-dom": "^5.2.0",
|
|
44
|
-
"sass-loader": "^11.0.1",
|
|
45
|
-
"seamless-scroll-polyfill": "^1.2.3",
|
|
46
|
-
"style-loader": "^2.0.0",
|
|
47
|
-
"terser-webpack-plugin": "^5.1.1",
|
|
48
|
-
"typescript": "^4.2.3",
|
|
49
|
-
"uglifyjs-webpack-plugin": "^2.2.0",
|
|
50
|
-
"uuid": "^8.3.2",
|
|
51
|
-
"webpack": "^5.24.4",
|
|
52
|
-
"webpack-cli": "^4.5.0",
|
|
53
|
-
"source-map-loader": "^2.0.1",
|
|
54
|
-
"@types/react-helmet": "^6.1.0",
|
|
55
|
-
"react-helmet": "^6.1.0",
|
|
56
|
-
"@sentry/react": "^6.2.1",
|
|
57
|
-
"dompurify": "^2.2.6",
|
|
58
|
-
"react-rnd": "^10.2.4"
|
|
59
|
-
},
|
|
60
13
|
"browserslist": {
|
|
61
14
|
"0": ">0.2%",
|
|
62
15
|
"1": "not dead",
|
|
63
16
|
"2": "ie >= 11"
|
|
64
17
|
},
|
|
65
18
|
"devDependencies": {
|
|
66
|
-
"@sentry/react": "^6.2.2",
|
|
67
19
|
"@types/js-cookie": "^2.2.6",
|
|
68
20
|
"@types/lodash": "^4.14.168",
|
|
69
21
|
"@types/node": "^14.14.34",
|
|
70
22
|
"@types/react": "^17.0.3",
|
|
71
23
|
"@types/react-dom": "^17.0.2",
|
|
72
|
-
"@types/react-helmet": "^6.1.0",
|
|
73
24
|
"@types/react-router-dom": "^5.1.7",
|
|
74
25
|
"@types/uuid": "^8.3.0",
|
|
75
|
-
"
|
|
26
|
+
"@types/react-color": "^3.0.6",
|
|
27
|
+
"@types/dompurify": "^2.2.3",
|
|
76
28
|
"classnames": "^2.2.6",
|
|
77
|
-
"compression-webpack-plugin": "^7.1.2",
|
|
78
29
|
"cross-env": "^7.0.3",
|
|
79
|
-
"css-loader": "^5.1.2",
|
|
80
|
-
"cssnano": "^4.1.10",
|
|
81
|
-
"cssnano-preset-advanced": "^4.0.7",
|
|
82
|
-
"file-loader": "^6.2.0",
|
|
83
|
-
"html-webpack-plugin": "^5.3.1",
|
|
84
30
|
"js-cookie": "^2.2.1",
|
|
85
31
|
"lodash": "^4.17.21",
|
|
86
|
-
"mini-css-extract-plugin": "^1.3.9",
|
|
87
32
|
"moment": "^2.29.1",
|
|
88
|
-
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
|
89
|
-
"prerender-spa-plugin-next": "^4.1.5",
|
|
90
|
-
"query-string": "^6.14.1",
|
|
91
33
|
"react": "^17.0.1",
|
|
92
34
|
"react-dom": "^17.0.1",
|
|
93
|
-
"react-
|
|
35
|
+
"react-color": "^2.19.3",
|
|
94
36
|
"react-router-dom": "^5.2.0",
|
|
95
|
-
"
|
|
96
|
-
"seamless-scroll-polyfill": "^1.2.3",
|
|
97
|
-
"source-map-loader": "^2.0.1",
|
|
98
|
-
"style-loader": "^2.0.0",
|
|
99
|
-
"terser-webpack-plugin": "^5.1.1",
|
|
37
|
+
"react-rnd": "^10.3.5",
|
|
100
38
|
"typescript": "^4.2.3",
|
|
101
|
-
"uglifyjs-webpack-plugin": "^2.2.0",
|
|
102
39
|
"uuid": "^8.3.2",
|
|
103
|
-
"
|
|
104
|
-
"webpack-cli": "^4.5.0",
|
|
105
|
-
"webpack-dev-server": "^4.3.1"
|
|
40
|
+
"dompurify": "^2.3.3"
|
|
106
41
|
}
|
|
107
42
|
}
|
|
@@ -31,6 +31,30 @@ function getBottomItems(items: SidebarItem[]): SidebarItem[] {
|
|
|
31
31
|
return items.filter((item: SidebarItem) => item.onBottom)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function getActiveItem(items: SidebarItem[], pathname: string): SidebarItem | null {
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (itemIsActive(item, pathname)) return item;
|
|
37
|
+
if (Array.isArray(item.children)) {
|
|
38
|
+
const activeItem = getActiveItem(item.children, pathname);
|
|
39
|
+
if (activeItem) return activeItem;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function itemIsActive(item: SidebarItem, pathname: string): boolean {
|
|
46
|
+
if (Array.isArray(item.route) && item.route.includes(pathname)) return true;
|
|
47
|
+
else if (item.route === pathname) return true;
|
|
48
|
+
else if (typeof item.route === 'string' && item.exactRoute === false && pathname.includes(item.route)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function itemHasActiveId(item: SidebarItem, activeId: string): boolean {
|
|
53
|
+
if (item.id === activeId) return true;
|
|
54
|
+
else if (Array.isArray(item.children) && item.children.find((child: SidebarItem) => itemHasActiveId(child, activeId))) return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
export default function SidebarMenu(props: Props) {
|
|
35
59
|
const context = useContext(ControlContext),
|
|
36
60
|
location = useLocation(),
|
|
@@ -42,27 +66,56 @@ export default function SidebarMenu(props: Props) {
|
|
|
42
66
|
|
|
43
67
|
function _onClickItem(item: SidebarItem) {
|
|
44
68
|
return function (evt: React.MouseEvent) {
|
|
69
|
+
evt.stopPropagation();
|
|
45
70
|
setActiveItem(item.id);
|
|
46
71
|
onClickItem(item)(evt);
|
|
47
72
|
}
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
function renderItem(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
function renderItem(isChild: boolean = false) {
|
|
76
|
+
return function (item: SidebarItem) {
|
|
77
|
+
const {className, icon, id, label, children} = item,
|
|
78
|
+
isActive = itemHasActiveId(item, activeItem),
|
|
79
|
+
cls = classNames(className, mode === SideMenuMode.COMPACT ? 'side-bar-item' : 'side-bar-item-full',
|
|
80
|
+
isChild && 'child', children !== undefined && 'with-children');
|
|
81
|
+
return (
|
|
82
|
+
<div key={id} className={cls} onClick={_onClickItem(item)}>
|
|
83
|
+
{renderIcon(isActive, isChild, label, icon)}
|
|
84
|
+
{mode === SideMenuMode.COMPACT && <div className='active-indicator'/>}
|
|
85
|
+
<span className={classNames('item-label', isActive && 'active')}>{label}</span>
|
|
86
|
+
{children !== undefined && (
|
|
87
|
+
<>
|
|
88
|
+
{isActive && <div className='line'/>}
|
|
89
|
+
{renderChildren(isActive)(children)}
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
61
95
|
}
|
|
62
96
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
function renderChildren(isActive: boolean) {
|
|
98
|
+
return function (children: SidebarItem[]) {
|
|
99
|
+
const display = isActive ? 'block' : 'none';
|
|
100
|
+
return (
|
|
101
|
+
<div className='children-container' style={{display}}>
|
|
102
|
+
{children.map(renderItem(true))}
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderIcon(isActive: boolean, isChild: boolean, label: string, icon?: string | ReactNode): ReactNode {
|
|
109
|
+
const className = classNames('item-icon', isActive && 'active');
|
|
110
|
+
if (icon) {
|
|
111
|
+
if (typeof icon === 'string') return <AwesomeIcon name={icon} className={className}/>;
|
|
112
|
+
else if (React.isValidElement(icon))
|
|
113
|
+
return React.cloneElement(icon, {className: classNames(className, icon.props.className)});
|
|
114
|
+
}
|
|
115
|
+
if (label.length) {
|
|
116
|
+
if (isChild) return <span/>;
|
|
117
|
+
return <span className={classNames(className, 'letter')}>{label[0]}</span>
|
|
118
|
+
}
|
|
66
119
|
return null;
|
|
67
120
|
}
|
|
68
121
|
|
|
@@ -78,26 +131,10 @@ export default function SidebarMenu(props: Props) {
|
|
|
78
131
|
|
|
79
132
|
function _setActiveItem() {
|
|
80
133
|
const {pathname} = location;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
let found: boolean = false;
|
|
84
|
-
if (item.route) {
|
|
85
|
-
if (Array.isArray(item.route) && item.route.includes(pathname)) found = true;
|
|
86
|
-
else if (item.route === pathname) found = true;
|
|
87
|
-
else if (typeof item.route === 'string' && item.exactRoute === false && pathname.includes(item.route)) found = true;
|
|
88
|
-
if (found) {
|
|
89
|
-
setActiveItem(item.id);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
134
|
+
const activeItem = getActiveItem(items, pathname);
|
|
135
|
+
if (activeItem) setActiveItem(activeItem.id);
|
|
94
136
|
}
|
|
95
137
|
|
|
96
|
-
// function getBarWidth(): number {
|
|
97
|
-
// if (sideBar.current) return sideBar.current.getBoundingClientRect().width;
|
|
98
|
-
// return 0;
|
|
99
|
-
// }
|
|
100
|
-
|
|
101
138
|
useEffect(() => {
|
|
102
139
|
setColors();
|
|
103
140
|
defer(_setActiveItem);
|
|
@@ -111,112 +148,11 @@ export default function SidebarMenu(props: Props) {
|
|
|
111
148
|
<div className={classNames(className, 'dvr-side-bar-menu', mode === SideMenuMode.FULL && 'full')}
|
|
112
149
|
ref={sideBar} id={componentId.current}>
|
|
113
150
|
<div>
|
|
114
|
-
{getTopItems(items).map(renderItem)}
|
|
151
|
+
{getTopItems(items).map(renderItem())}
|
|
115
152
|
</div>
|
|
116
153
|
<div>
|
|
117
|
-
{getBottomItems(items).map(renderItem)}
|
|
154
|
+
{getBottomItems(items).map(renderItem())}
|
|
118
155
|
</div>
|
|
119
156
|
</div>
|
|
120
157
|
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// class SidebarMenu extends PureComponent<Props, State> {
|
|
124
|
-
// static contextType = ControlContext;
|
|
125
|
-
// static defaultProps = {
|
|
126
|
-
// controlled: false,
|
|
127
|
-
// mode: SideMenuMode.COMPACT,
|
|
128
|
-
// }
|
|
129
|
-
//
|
|
130
|
-
// private bar: HTMLDivElement;
|
|
131
|
-
// private readonly id = generateComponentId(this.props.id);
|
|
132
|
-
// state: State = {
|
|
133
|
-
// activeItem: this.props.activeItem || '',
|
|
134
|
-
// };
|
|
135
|
-
//
|
|
136
|
-
// getTopItems = (): SidebarItem[] => this.props.items.filter((item: SidebarItem) => !item.onBottom)
|
|
137
|
-
// getBottomItems = (): SidebarItem[] => this.props.items.filter((item: SidebarItem) => item.onBottom)
|
|
138
|
-
//
|
|
139
|
-
// renderItem = (item: SidebarItem) => {
|
|
140
|
-
// const {className, icon, id, label} = item, {onClickItem, mode} = this.props, {activeItem} = this.state;
|
|
141
|
-
// return (
|
|
142
|
-
// <div key={id}
|
|
143
|
-
// className={classNames(className, mode === SideMenuMode.COMPACT ? 'side-bar-item' : 'side-bar-item-full', activeItem === id && 'active')}
|
|
144
|
-
// onClick={onClickItem(item)}>
|
|
145
|
-
// {this.renderIcon(label, icon)}
|
|
146
|
-
// {mode === SideMenuMode.COMPACT && <div className='active-indicator'/>}
|
|
147
|
-
// <span className='item-label'>{label}</span>
|
|
148
|
-
// </div>
|
|
149
|
-
// )
|
|
150
|
-
// };
|
|
151
|
-
//
|
|
152
|
-
// renderIcon = (label: string, icon?: string | React.ReactNode): React.ReactNode => {
|
|
153
|
-
// if (icon) return typeof icon === 'string' ? <AwesomeIcon name={icon} className='item-icon'/> : icon;
|
|
154
|
-
// if (label.length) return <span className='item-icon letter'>{label[0]}</span>
|
|
155
|
-
// return null;
|
|
156
|
-
// };
|
|
157
|
-
//
|
|
158
|
-
// setColorVars = () => {
|
|
159
|
-
// const {baseColor, contrastColor} = this.props;
|
|
160
|
-
// if (this.bar) {
|
|
161
|
-
// const base: string = baseColor || this.context.baseColor,
|
|
162
|
-
// contrast: string = contrastColor || this.context.contrastColor;
|
|
163
|
-
// this.bar.style.setProperty('--base-color', base);
|
|
164
|
-
// this.bar.style.setProperty('--contrast-color', contrast);
|
|
165
|
-
// }
|
|
166
|
-
// };
|
|
167
|
-
//
|
|
168
|
-
// setActiveItem = () => {
|
|
169
|
-
// const route = this.props.location.pathname, {items} = this.props;
|
|
170
|
-
// for (const item of items) {
|
|
171
|
-
// let found: boolean = false;
|
|
172
|
-
// if (item.route) {
|
|
173
|
-
// if (Array.isArray(item.route) && item.route.includes(route)) found = true;
|
|
174
|
-
// else if (item.route === route) found = true;
|
|
175
|
-
// else if (typeof item.route === 'string' && item.exactRoute === false && route.includes(item.route)) found = true;
|
|
176
|
-
// if (found) {
|
|
177
|
-
// this.setState({activeItem: item.id});
|
|
178
|
-
// return;
|
|
179
|
-
// }
|
|
180
|
-
// }
|
|
181
|
-
// }
|
|
182
|
-
// }
|
|
183
|
-
//
|
|
184
|
-
// getWidth = (): number => {
|
|
185
|
-
// if (this.bar) return this.bar.getBoundingClientRect().width;
|
|
186
|
-
// return 0;
|
|
187
|
-
// };
|
|
188
|
-
//
|
|
189
|
-
// componentDidMount = () => {
|
|
190
|
-
// this.setColorVars();
|
|
191
|
-
// directTimeout(this.setActiveItem);
|
|
192
|
-
// };
|
|
193
|
-
//
|
|
194
|
-
// componentDidUpdate = (prevProps: Props) => {
|
|
195
|
-
// const {activeItem, controlled, location} = this.props, prevItem = prevProps.activeItem,
|
|
196
|
-
// prevLocation = prevProps.location;
|
|
197
|
-
// if (controlled) {
|
|
198
|
-
// if (activeItem !== prevItem) this.setState({activeItem: activeItem || ''});
|
|
199
|
-
// } else {
|
|
200
|
-
// if (location.pathname !== prevLocation.pathname) this.setActiveItem();
|
|
201
|
-
// }
|
|
202
|
-
// }
|
|
203
|
-
//
|
|
204
|
-
// render = () => {
|
|
205
|
-
// const {className, mode} = this.props;
|
|
206
|
-
// return (
|
|
207
|
-
// <div className={classNames(className, 'dvr-side-bar-menu', mode === SideMenuMode.FULL && 'full')}
|
|
208
|
-
// ref={(ref: HTMLDivElement) => {
|
|
209
|
-
// this.bar = ref
|
|
210
|
-
// }} id={this.id}>
|
|
211
|
-
// <div>
|
|
212
|
-
// {this.getTopItems().map(this.renderItem)}
|
|
213
|
-
// </div>
|
|
214
|
-
// <div>
|
|
215
|
-
// {this.getBottomItems().map(this.renderItem)}
|
|
216
|
-
// </div>
|
|
217
|
-
// </div>
|
|
218
|
-
// )
|
|
219
|
-
// };
|
|
220
|
-
// }
|
|
221
|
-
//
|
|
222
|
-
// export default withRouter(SidebarMenu);
|
|
158
|
+
}
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
margin: 2rem 1rem;
|
|
99
99
|
align-items: center;
|
|
100
100
|
cursor: pointer;
|
|
101
|
+
position: relative;
|
|
101
102
|
|
|
102
103
|
.item-icon {
|
|
103
104
|
color: #686869;
|
|
@@ -105,23 +106,44 @@
|
|
|
105
106
|
transition: color .2s ease-in-out;
|
|
106
107
|
justify-self: center;
|
|
107
108
|
opacity: .5;
|
|
109
|
+
|
|
110
|
+
&.active {
|
|
111
|
+
color: var(--contrast-color);
|
|
112
|
+
opacity: 1;
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
.item-label {
|
|
111
117
|
color: #686869;
|
|
112
118
|
transition: color .2s ease-in-out;
|
|
113
119
|
white-space: nowrap;
|
|
114
|
-
}
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
color:
|
|
119
|
-
opacity: 1;
|
|
121
|
+
&.active {
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
color: black;
|
|
120
124
|
}
|
|
125
|
+
}
|
|
121
126
|
|
|
127
|
+
.line {
|
|
128
|
+
width: 2px;
|
|
129
|
+
position: absolute;
|
|
130
|
+
height: calc(100% - 1.5rem);
|
|
131
|
+
top: 2rem;
|
|
132
|
+
left: 1rem;
|
|
133
|
+
background-color: var(--contrast-color);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&.child {
|
|
122
137
|
.item-label {
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
padding-left: 1rem;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.children-container {
|
|
143
|
+
padding-top: 1rem;
|
|
144
|
+
|
|
145
|
+
.side-bar-item-full {
|
|
146
|
+
margin: 0;
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
|
|
@@ -130,6 +152,10 @@
|
|
|
130
152
|
color: black;
|
|
131
153
|
}
|
|
132
154
|
}
|
|
155
|
+
|
|
156
|
+
&.with-children {
|
|
157
|
+
margin-bottom: -.5rem;
|
|
158
|
+
}
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
}
|
|
@@ -6,6 +6,7 @@ import React from 'react';
|
|
|
6
6
|
import {Snackbar} from "./snackbar";
|
|
7
7
|
import {CustomAppEvent, DialogConfig, Snack} from "../util/interfaces";
|
|
8
8
|
import WithEvents from '../events/withEvents';
|
|
9
|
+
import { delay } from 'lodash';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
containerClass: string;
|
|
@@ -13,6 +14,8 @@ interface Props {
|
|
|
13
14
|
activeTime: number;
|
|
14
15
|
backgroundColor?: string;
|
|
15
16
|
textColor?: string;
|
|
17
|
+
maxSnacks?: number;
|
|
18
|
+
noDuplicates?: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
interface State {
|
|
@@ -28,7 +31,7 @@ export default class SnackbarController extends React.Component<Props, State> {
|
|
|
28
31
|
textClass: '',
|
|
29
32
|
};
|
|
30
33
|
|
|
31
|
-
state = {
|
|
34
|
+
state: State = {
|
|
32
35
|
active: false,
|
|
33
36
|
snack: null,
|
|
34
37
|
config: {}
|
|
@@ -38,7 +41,12 @@ export default class SnackbarController extends React.Component<Props, State> {
|
|
|
38
41
|
timeout: number | null = null;
|
|
39
42
|
|
|
40
43
|
onAddSnack = (snack: Snack) => {
|
|
44
|
+
const {maxSnacks, noDuplicates} = this.props;
|
|
45
|
+
if (noDuplicates && this.isDuplicate(snack)) return;
|
|
41
46
|
this.snackQueue.push(snack);
|
|
47
|
+
if (maxSnacks !== undefined)
|
|
48
|
+
while (this.snackQueue.length > maxSnacks - 1)
|
|
49
|
+
this.snackQueue.shift();
|
|
42
50
|
if (!this.state.active)
|
|
43
51
|
this.activate();
|
|
44
52
|
};
|
|
@@ -49,10 +57,19 @@ export default class SnackbarController extends React.Component<Props, State> {
|
|
|
49
57
|
this.deactivate();
|
|
50
58
|
};
|
|
51
59
|
|
|
60
|
+
isDuplicate = (snack: Snack): boolean => {
|
|
61
|
+
return this.state.snack?.text === snack.text || this.snackQueue.pop()?.text === snack.text;
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
deactivate = () => {
|
|
53
65
|
this.setState({active: false}, () => {
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
delay(() => {
|
|
67
|
+
// Clear snack after 200ms (css transition)
|
|
68
|
+
this.setState({snack: null}, () => {
|
|
69
|
+
if (this.snackQueue.length > 0)
|
|
70
|
+
this.timeout = window.setTimeout(this.activate, 500);
|
|
71
|
+
})
|
|
72
|
+
}, 200);
|
|
56
73
|
})
|
|
57
74
|
};
|
|
58
75
|
|
|
@@ -8,14 +8,14 @@ import React, {
|
|
|
8
8
|
PureComponent
|
|
9
9
|
} from 'react';
|
|
10
10
|
import classNames from "classnames";
|
|
11
|
-
import {ElementPosition, ErrorType, OrnamentShape
|
|
11
|
+
import {ElementPosition, ErrorType, OrnamentShape} from '../util/interfaces';
|
|
12
12
|
import AwesomeIcon from '../icon/awesomeIcon';
|
|
13
|
-
import {
|
|
13
|
+
import {directTimeout} from '../util/componentUtil';
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
onChange: ChangeEventHandler;
|
|
17
|
-
onFocus
|
|
18
|
-
onBlur
|
|
17
|
+
onFocus?: FocusEventHandler;
|
|
18
|
+
onBlur?: FocusEventHandler;
|
|
19
19
|
onKeyDown: KeyboardEventHandler;
|
|
20
20
|
onClearInput?: MouseEventHandler;
|
|
21
21
|
value: string | number;
|
|
@@ -62,13 +62,15 @@ export default class DvrdInput extends PureComponent<Props, State> {
|
|
|
62
62
|
this.input.selectionEnd = value.toString().length;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
onFocus
|
|
65
|
+
if (onFocus)
|
|
66
|
+
onFocus(evt);
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
onBlurInput = (evt: React.FocusEvent) => {
|
|
69
70
|
const {disabled, onBlur} = this.props;
|
|
70
71
|
if (!disabled) this.setState({active: false});
|
|
71
|
-
onBlur
|
|
72
|
+
if (onBlur)
|
|
73
|
+
onBlur(evt);
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
hasError = (): boolean => this.props.error !== undefined && this.props.error !== null;
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import React, {
|
|
6
|
-
FocusEventHandler,
|
|
7
|
-
|
|
6
|
+
FocusEventHandler,
|
|
7
|
+
InputHTMLAttributes,
|
|
8
|
+
KeyboardEventHandler,
|
|
9
|
+
MouseEventHandler,
|
|
10
|
+
TextareaHTMLAttributes,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
useState
|
|
8
14
|
} from 'react';
|
|
9
15
|
import {generateComponentId} from '../util/componentUtil';
|
|
10
16
|
import {enterPressed} from '../util/controlUtil';
|
|
@@ -40,62 +46,52 @@ export interface InputControllerProps {
|
|
|
40
46
|
step?: number;
|
|
41
47
|
area?: boolean;
|
|
42
48
|
noResize?: boolean;
|
|
49
|
+
unControlled?: boolean;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
export default
|
|
46
|
-
|
|
52
|
+
export default function DvrdInputController(props: InputControllerProps) {
|
|
53
|
+
const {
|
|
54
|
+
disabled, onChange, onEnter, onKeyDown, inputProps, placeholder, type, max, min, step, onFocus, onBlur,
|
|
55
|
+
autoFocus, error, label, ornaments, className, inputClassName, labelClassName, ornamentClassName, fullWidth,
|
|
56
|
+
errorClassName, autoSelect, area, noResize, onClearInput, unControlled
|
|
57
|
+
} = props,
|
|
58
|
+
componentId = useRef(generateComponentId(props.id)),
|
|
59
|
+
[value, setValue] = useState(props.value);
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!disabled) onChange(value, evt);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
onKeyDown = (evt: React.KeyboardEvent) => {
|
|
58
|
-
const {onEnter, disabled, onKeyDown} = this.props;
|
|
59
|
-
if (!disabled) {
|
|
60
|
-
if (enterPressed(evt) && onEnter) onEnter(evt);
|
|
61
|
-
else if (onKeyDown) onKeyDown(evt);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
onFocus = (evt: React.FocusEvent) => {
|
|
66
|
-
const {onFocus} = this.props;
|
|
67
|
-
if (onFocus) onFocus(evt);
|
|
68
|
-
};
|
|
61
|
+
function _onChange(evt: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
|
62
|
+
if (disabled) return;
|
|
63
|
+
const {value} = evt.target;
|
|
64
|
+
setValue(value);
|
|
65
|
+
onChange(value, evt);
|
|
66
|
+
}
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
68
|
+
function _onKeyDown(evt: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
|
69
|
+
if (disabled) return;
|
|
70
|
+
if (onEnter && enterPressed(evt)) onEnter(evt);
|
|
71
|
+
else if (onKeyDown) onKeyDown(evt);
|
|
72
|
+
}
|
|
74
73
|
|
|
75
|
-
getInputProps
|
|
76
|
-
const {inputProps, placeholder, type, max, min, step} = this.props;
|
|
74
|
+
function getInputProps(): InputHTMLAttributes<any> | undefined {
|
|
77
75
|
if (!inputProps && !placeholder && !type) return undefined;
|
|
78
76
|
const props: InputHTMLAttributes<any> = inputProps ?? {};
|
|
79
|
-
props.placeholder = placeholder
|
|
80
|
-
props.type = type
|
|
81
|
-
props.max = max
|
|
82
|
-
props.min = min
|
|
83
|
-
props.step = step
|
|
77
|
+
if (placeholder !== undefined) props.placeholder = placeholder;
|
|
78
|
+
if (type !== undefined) props.type = type;
|
|
79
|
+
if (max !== undefined) props.max = max;
|
|
80
|
+
if (min !== undefined) props.min = min;
|
|
81
|
+
if (step !== undefined) props.step = step;
|
|
84
82
|
return props;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
render = () => {
|
|
88
|
-
const {
|
|
89
|
-
value, label, ornaments, disabled, autoFocus, className, inputClassName, labelClassName,
|
|
90
|
-
ornamentClassName, error, errorClassName, autoSelect, fullWidth, area, noResize, onClearInput
|
|
91
|
-
} = this.props;
|
|
92
|
-
return (
|
|
93
|
-
<DvrdInput onChange={this.onChange} onFocus={this.onFocus} onKeyDown={this.onKeyDown} value={value}
|
|
94
|
-
disabled={disabled} autoFocus={autoFocus} error={error} label={label} ornaments={ornaments}
|
|
95
|
-
inputProps={this.getInputProps()} className={className} inputClassName={inputClassName}
|
|
96
|
-
labelClassName={labelClassName} ornamentClassName={ornamentClassName} fullWidth={fullWidth}
|
|
97
|
-
errorClassName={errorClassName} id={this.id} onBlur={this.onBlur} autoSelect={autoSelect}
|
|
98
|
-
area={area} noResize={noResize} onClearInput={onClearInput}/>
|
|
99
|
-
)
|
|
100
83
|
}
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if(!unControlled) setValue(props.value);
|
|
87
|
+
}, [props.value])
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<DvrdInput onChange={_onChange} onFocus={onFocus} onKeyDown={_onKeyDown} value={value}
|
|
91
|
+
disabled={disabled} autoFocus={autoFocus} error={error} label={label} ornaments={ornaments}
|
|
92
|
+
inputProps={getInputProps()} className={className} inputClassName={inputClassName}
|
|
93
|
+
labelClassName={labelClassName} ornamentClassName={ornamentClassName} fullWidth={fullWidth}
|
|
94
|
+
errorClassName={errorClassName} id={componentId.current} onBlur={onBlur} autoSelect={autoSelect}
|
|
95
|
+
area={area} noResize={noResize} onClearInput={onClearInput}/>
|
|
96
|
+
);
|
|
101
97
|
}
|
package/src/js/util/miscUtil.ts
CHANGED
|
@@ -156,14 +156,11 @@ export const copyClipboard = (text: string): Promise<void> => {
|
|
|
156
156
|
return window.navigator.clipboard.writeText(text);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
export
|
|
160
|
-
const webpSupported = webpIsSupported(), extension = webpSupported ? 'webp' : fallbackExtension;
|
|
159
|
+
export function webpSupported(): boolean {
|
|
161
160
|
try {
|
|
162
|
-
return
|
|
161
|
+
return document.createElement('canvas').toDataURL('image/webp')
|
|
162
|
+
.indexOf('data:image/webp') === 0;
|
|
163
163
|
} catch {
|
|
164
|
-
return
|
|
164
|
+
return false;
|
|
165
165
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const webpIsSupported = (): boolean => document.createElement('canvas').toDataURL('image/webp')
|
|
169
|
-
.indexOf('data:image/webp') === 0;
|
|
166
|
+
}
|
|
@@ -26,16 +26,21 @@ let abortController: AbortController;
|
|
|
26
26
|
let signal: AbortSignal;
|
|
27
27
|
let defaultHeaders: OutgoingHttpHeaders = {};
|
|
28
28
|
|
|
29
|
-
const getSignal = (): AbortSignal | null => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
29
|
+
// const getSignal = (): AbortSignal | null => {
|
|
30
|
+
// if ("AbortController" in window) {
|
|
31
|
+
// if (!abortController)
|
|
32
|
+
// abortController = new AbortController;
|
|
33
|
+
// if (!signal)
|
|
34
|
+
// signal = abortController.signal;
|
|
35
|
+
// return signal;
|
|
36
|
+
// }
|
|
37
|
+
// return null;
|
|
38
|
+
// };
|
|
39
|
+
|
|
40
|
+
function getAbortController(): AbortController | null {
|
|
41
|
+
if (!('AbortController' in window)) return null;
|
|
42
|
+
return new AbortController();
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
const responseDataIsSuccess = (data: ResponseData): boolean => data.status === 'success';
|
|
41
46
|
|
|
@@ -54,7 +59,7 @@ export const cancelAllFetch = () => {
|
|
|
54
59
|
}
|
|
55
60
|
};
|
|
56
61
|
|
|
57
|
-
export const sendFetch = (config: FetchOptions, legacySupport: boolean = false) => {
|
|
62
|
+
export const sendFetch = (config: FetchOptions, legacySupport: boolean = false): AbortController | null => {
|
|
58
63
|
config = createFetchConfig(config, legacySupport);
|
|
59
64
|
const {url, method, headers, data} = config, options: { [index: string]: any } = {
|
|
60
65
|
headers,
|
|
@@ -63,7 +68,9 @@ export const sendFetch = (config: FetchOptions, legacySupport: boolean = false)
|
|
|
63
68
|
cache: 'no-store',
|
|
64
69
|
}, baseUrl = config.baseUrl || window.settings.platformUrl;
|
|
65
70
|
if (!baseUrl) throw new Error('Base url is not set!');
|
|
66
|
-
|
|
71
|
+
const abortController = getAbortController();
|
|
72
|
+
if(abortController) options.signal = abortController.signal;
|
|
73
|
+
// if (getSignal()) options['signal'] = getSignal();
|
|
67
74
|
options['mode'] = 'cors';
|
|
68
75
|
fetch(baseUrl + url, options).then((response: Response) => {
|
|
69
76
|
const {status} = response;
|
|
@@ -101,10 +108,12 @@ export const sendFetch = (config: FetchOptions, legacySupport: boolean = false)
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
}).catch((reason: any) => {
|
|
104
|
-
if (reason.code === DOMException.ABORT_ERR) {/*Request aborted*/
|
|
111
|
+
if (reason.code === DOMException.ABORT_ERR) {/*Request aborted*/
|
|
112
|
+
} else if (config.errorCallback) {
|
|
105
113
|
config.errorCallback(reason);
|
|
106
114
|
}
|
|
107
115
|
});
|
|
116
|
+
return abortController;
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
const createFetchConfig = (options: FetchOptions, legacySupport: boolean = true): FetchOptions => {
|