wini-web-components 0.1.0
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/README.md +70 -0
- package/package.json +62 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +43 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/App.css +38 -0
- package/src/App.js +25 -0
- package/src/App.test.js +8 -0
- package/src/component/calendar/calendar.css +133 -0
- package/src/component/calendar/calendar.tsx +448 -0
- package/src/component/checkbox/checkbox.css +46 -0
- package/src/component/checkbox/checkbox.tsx +59 -0
- package/src/component/component-status.tsx +53 -0
- package/src/component/date-picker/date-picker.css +114 -0
- package/src/component/date-picker/date-picker.tsx +363 -0
- package/src/component/dialog/dialog.css +125 -0
- package/src/component/dialog/dialog.tsx +91 -0
- package/src/component/export-component.js +48 -0
- package/src/component/import-file/import-file.css +86 -0
- package/src/component/import-file/import-file.tsx +154 -0
- package/src/component/infinite-scroll/infinite-scroll.css +37 -0
- package/src/component/infinite-scroll/infinite-scroll.tsx +33 -0
- package/src/component/input-multi-select/input-multi-select.css +127 -0
- package/src/component/input-multi-select/input-multi-select.tsx +261 -0
- package/src/component/pagination/pagination.css +69 -0
- package/src/component/pagination/pagination.tsx +65 -0
- package/src/component/popup/popup.css +90 -0
- package/src/component/popup/popup.tsx +105 -0
- package/src/component/progress-bar/progress-bar.css +47 -0
- package/src/component/progress-bar/progress-bar.tsx +34 -0
- package/src/component/progress-circle/progress-circle.css +47 -0
- package/src/component/progress-circle/progress-circle.tsx +24 -0
- package/src/component/radio-button/radio-button.css +49 -0
- package/src/component/radio-button/radio-button.tsx +59 -0
- package/src/component/rating/rating.css +11 -0
- package/src/component/rating/rating.tsx +65 -0
- package/src/component/select1/select1.css +155 -0
- package/src/component/select1/select1.tsx +241 -0
- package/src/component/slider/slider.css +16 -0
- package/src/component/slider/slider.tsx +87 -0
- package/src/component/switch/switch.css +53 -0
- package/src/component/switch/switch.tsx +67 -0
- package/src/component/table/table.css +74 -0
- package/src/component/table/table.tsx +99 -0
- package/src/component/text/text.css +16 -0
- package/src/component/text/text.tsx +21 -0
- package/src/component/text-area/text-area.css +63 -0
- package/src/component/text-area/text-area.tsx +55 -0
- package/src/component/text-field/text-field.css +64 -0
- package/src/component/text-field/text-field.tsx +63 -0
- package/src/component/toast-noti/toast-noti.css +0 -0
- package/src/component/toast-noti/toast-noti.tsx +21 -0
- package/src/index.css +13 -0
- package/src/index.js +17 -0
- package/src/logo.svg +1 -0
- package/src/reportWebVitals.js +13 -0
- package/src/setupTests.js +5 -0
- package/src/skin/color.css +17 -0
- package/src/skin/typography.css +378 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { faChevronDown, faChevronUp, faClose, faSearch } from '@fortawesome/free-solid-svg-icons'
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
3
|
+
import React, { CSSProperties } from 'react'
|
|
4
|
+
import ReactDOM from 'react-dom'
|
|
5
|
+
import './input-multi-select.css'
|
|
6
|
+
import { Checkbox, Text } from '../export-component'
|
|
7
|
+
|
|
8
|
+
interface SelectMultipleProps {
|
|
9
|
+
value?: Array<string | number>,
|
|
10
|
+
options: Required<Array<{ id: string | number, name: string }>>,
|
|
11
|
+
onChange?: (value?: Array<string | number>) => void,
|
|
12
|
+
placeholder?: string,
|
|
13
|
+
disabled: boolean,
|
|
14
|
+
className?: string,
|
|
15
|
+
helperText?: string,
|
|
16
|
+
helperTextColor?: string,
|
|
17
|
+
style?: CSSProperties,
|
|
18
|
+
handleSearch?: (e: string) => Promise<Array<{ id: string | number, name: string }>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SelectMultipleState {
|
|
22
|
+
value: Array<string | number>,
|
|
23
|
+
offset: DOMRect,
|
|
24
|
+
isOpen: boolean,
|
|
25
|
+
onSelect: any,
|
|
26
|
+
search?: Array<{ id: string | number, name: string }>,
|
|
27
|
+
style?: Object
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class SelectMultiple extends React.Component<SelectMultipleProps, SelectMultipleState> {
|
|
31
|
+
constructor(props: SelectMultipleProps) {
|
|
32
|
+
super(props)
|
|
33
|
+
this.state = {
|
|
34
|
+
value: props.value ?? [],
|
|
35
|
+
offset: {
|
|
36
|
+
x: 0,
|
|
37
|
+
y: 0,
|
|
38
|
+
height: 0,
|
|
39
|
+
width: 0,
|
|
40
|
+
bottom: 0,
|
|
41
|
+
left: 0,
|
|
42
|
+
right: 0,
|
|
43
|
+
top: 0,
|
|
44
|
+
toJSON: function () {
|
|
45
|
+
throw new Error('Function not implemented.')
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
isOpen: false,
|
|
49
|
+
onSelect: null,
|
|
50
|
+
}
|
|
51
|
+
this.onCheck = this.onCheck.bind(this)
|
|
52
|
+
this.search = this.search.bind(this);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private onCheck(item: { id: string | number, name: string }) {
|
|
56
|
+
let newValue: Array<string | number> = []
|
|
57
|
+
if (this.state.value.includes(item.id)) {
|
|
58
|
+
newValue = this.state.value.filter(vl => vl !== item.id)
|
|
59
|
+
} else {
|
|
60
|
+
newValue = [...this.state.value, item.id]
|
|
61
|
+
}
|
|
62
|
+
this.setState({
|
|
63
|
+
...this.state,
|
|
64
|
+
value: newValue
|
|
65
|
+
})
|
|
66
|
+
if (this.props.onChange) this.props.onChange(newValue)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async search(ev: React.ChangeEvent<HTMLInputElement>) {
|
|
70
|
+
if (ev.target.value.trim().length) {
|
|
71
|
+
if (this.props?.handleSearch) {
|
|
72
|
+
const res = await this.props.handleSearch(ev.target.value.trim())
|
|
73
|
+
this.setState({
|
|
74
|
+
...this.state,
|
|
75
|
+
search: res
|
|
76
|
+
})
|
|
77
|
+
} else {
|
|
78
|
+
this.setState({
|
|
79
|
+
...this.state,
|
|
80
|
+
search: this.props.options.filter(e => e.name.toLowerCase().includes(ev.target.value.trim().toLowerCase()))
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
this.setState({
|
|
85
|
+
...this.state,
|
|
86
|
+
search: undefined
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
componentDidUpdate(prevProps: SelectMultipleProps, prevState: SelectMultipleState) {
|
|
92
|
+
if (prevProps.value !== this.props.value) {
|
|
93
|
+
this.setState({
|
|
94
|
+
...this.state,
|
|
95
|
+
value: this.props.value ?? []
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (prevState.isOpen !== this.state.isOpen && this.state.isOpen) {
|
|
99
|
+
const thisPopupRect = document.body.querySelector('.select-multi-popup')?.getBoundingClientRect()
|
|
100
|
+
if (thisPopupRect) {
|
|
101
|
+
let style: { top?: string, left?: string, right?: string, bottom?: string, width?: string, height?: string } | undefined;
|
|
102
|
+
if (thisPopupRect.right > document.body.offsetWidth) {
|
|
103
|
+
style = {
|
|
104
|
+
top: this.state.offset.y + this.state.offset.height + 2 + 'px',
|
|
105
|
+
width: `${this.state.offset.width}px`,
|
|
106
|
+
right: document.body.offsetWidth - this.state.offset.right + 'px'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (thisPopupRect.bottom - 12 > document.body.offsetHeight) {
|
|
110
|
+
style = style ? {
|
|
111
|
+
...style,
|
|
112
|
+
top: undefined,
|
|
113
|
+
bottom: document.body.offsetHeight - this.state.offset.bottom + 'px'
|
|
114
|
+
} : {
|
|
115
|
+
left: this.state.offset.x + 'px',
|
|
116
|
+
width: `${this.state.offset.width}px`,
|
|
117
|
+
bottom: document.body.offsetHeight - this.state.offset.bottom + 'px'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (style) this.setState({
|
|
121
|
+
...this.state,
|
|
122
|
+
style: style
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render() {
|
|
129
|
+
return <div
|
|
130
|
+
className={`select-multi-container row ${this.props.disabled ? 'disabled' : ''} ${this.props.helperText?.length && 'helper-text'}`}
|
|
131
|
+
helper-text={this.props.helperText}
|
|
132
|
+
style={this.props.style ? { ...({ '--helper-text-color': this.props.helperTextColor ?? '#e14337' } as CSSProperties), ...this.props.style } : ({ '--helper-text-color': this.props.helperTextColor ?? '#e14337' } as CSSProperties)}
|
|
133
|
+
onClick={ev => {
|
|
134
|
+
if (!this.state.isOpen) {
|
|
135
|
+
this.setState({
|
|
136
|
+
...this.state,
|
|
137
|
+
isOpen: true,
|
|
138
|
+
style: undefined,
|
|
139
|
+
offset: ((ev.target as HTMLElement).closest('.select-multi-container') ?? (ev.target as HTMLElement)).getBoundingClientRect(),
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div className='row' style={{ overflow: 'auto hidden', flex: 1, width: '100%', gap: '0.8rem' }}>
|
|
145
|
+
{this.state.value?.length ? (
|
|
146
|
+
this.state.value.map(item => {
|
|
147
|
+
const optionItem = this.props.options.find(e => e.id === item)
|
|
148
|
+
return <div
|
|
149
|
+
key={item}
|
|
150
|
+
className='selected-item-value row'
|
|
151
|
+
onClick={(ev) => {
|
|
152
|
+
ev.stopPropagation()
|
|
153
|
+
let newValue = this.state.value.filter(vl => vl !== item)
|
|
154
|
+
this.setState({
|
|
155
|
+
...this.state,
|
|
156
|
+
value: newValue,
|
|
157
|
+
isOpen: true,
|
|
158
|
+
style: undefined,
|
|
159
|
+
offset: ((ev.target as HTMLElement).closest('.select-multi-container') ?? (ev.target as HTMLElement)).getBoundingClientRect(),
|
|
160
|
+
})
|
|
161
|
+
if (this.props.onChange) this.props.onChange(newValue)
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<div className='selected-value-title'>{optionItem?.name}</div>
|
|
165
|
+
<FontAwesomeIcon icon={faClose} style={{ color: '#161D24E5', fontSize: '1.2rem' }} />
|
|
166
|
+
</div>
|
|
167
|
+
})
|
|
168
|
+
) : (
|
|
169
|
+
<div className='select-multi-placeholder'>
|
|
170
|
+
{this.props.placeholder}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
<FontAwesomeIcon
|
|
175
|
+
icon={this.state.isOpen ? faChevronUp : faChevronDown}
|
|
176
|
+
style={{ fontSize: '1.2rem', color: '#888' }}
|
|
177
|
+
/>
|
|
178
|
+
{this.state.isOpen &&
|
|
179
|
+
ReactDOM.createPortal(
|
|
180
|
+
<div
|
|
181
|
+
className='select-multi-popup col'
|
|
182
|
+
style={this.state.style ?? {
|
|
183
|
+
top: this.state.offset.y + this.state.offset.height + 2 + 'px',
|
|
184
|
+
left: this.state.offset.x + 'px',
|
|
185
|
+
width: this.state.offset.width,
|
|
186
|
+
}}
|
|
187
|
+
onMouseOver={ev => {
|
|
188
|
+
this.setState({
|
|
189
|
+
...this.state,
|
|
190
|
+
onSelect: ev.target
|
|
191
|
+
})
|
|
192
|
+
}}
|
|
193
|
+
onMouseOut={() =>
|
|
194
|
+
this.setState({
|
|
195
|
+
...this.state,
|
|
196
|
+
onSelect: null
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
<div className='row header-search'>
|
|
201
|
+
<input
|
|
202
|
+
autoFocus={true}
|
|
203
|
+
placeholder={'Tìm kiếm'}
|
|
204
|
+
onChange={this.search}
|
|
205
|
+
onBlur={ev => {
|
|
206
|
+
if (this.state.onSelect) {
|
|
207
|
+
ev.target.focus()
|
|
208
|
+
} else {
|
|
209
|
+
this.setState({
|
|
210
|
+
...this.state,
|
|
211
|
+
isOpen: false,
|
|
212
|
+
onSelect: null
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
<FontAwesomeIcon icon={faSearch} style={{ fontSize: '1.2rem', color: '#161D24D9' }} />
|
|
218
|
+
</div>
|
|
219
|
+
<div style={{ padding: '1.2rem 1.6rem', width: '100%', borderTop: '1px solid #161D2414', borderBottom: '1px solid #161D2414' }}>
|
|
220
|
+
{(() => {
|
|
221
|
+
const _list = (this.state.search ?? this.props.options ?? [])
|
|
222
|
+
const isSelectedAll = _list.every(item => this.state.value.some(vl => vl === item.id))
|
|
223
|
+
return <Text onClick={() => {
|
|
224
|
+
let newValue: Array<string | number> = []
|
|
225
|
+
if (_list.length) {
|
|
226
|
+
if (isSelectedAll) {
|
|
227
|
+
newValue = this.state.value.filter(vl => _list.every(item => vl !== item.id))
|
|
228
|
+
} else {
|
|
229
|
+
newValue = [...this.state.value, ..._list.filter(item => this.state.value.every(vl => vl !== item.id)).map(e => e.id)]
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.setState({ ...this.state, value: newValue })
|
|
233
|
+
if (this.props.onChange) this.props.onChange(newValue)
|
|
234
|
+
}} className='button-text-3' style={{ color: _list.length ? 'var(--primary-color)' : '#00204D99', }}>{_list.length && isSelectedAll ? 'Bỏ chọn tất cả' : 'Chọn tất cả'}</Text>
|
|
235
|
+
})()}
|
|
236
|
+
</div>
|
|
237
|
+
<div className='col select-body'>
|
|
238
|
+
{(this.state.search ?? this.props.options ?? []).map(
|
|
239
|
+
item => (
|
|
240
|
+
<div key={item.id} className='select-tile row'>
|
|
241
|
+
<Checkbox
|
|
242
|
+
value={this.state.value.some(vl => vl === item.id)}
|
|
243
|
+
onChange={() => { this.onCheck(item) }}
|
|
244
|
+
size={'2rem'}
|
|
245
|
+
/>
|
|
246
|
+
<div className='label-3'>
|
|
247
|
+
{item.name}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
)}
|
|
252
|
+
{(this.state.search?.length === 0 || this.props.options?.length === 0) && (
|
|
253
|
+
<div className='no-results-found'>No result found</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</div>,
|
|
257
|
+
document.body
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
.custom-pagination {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
gap: 1.6rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.custom-pagination>.row {
|
|
9
|
+
gap: 1.6rem;
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.items-per-page-container {
|
|
14
|
+
gap: 8px;
|
|
15
|
+
height: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.pagination {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: row !important;
|
|
21
|
+
height: 4rem !important;
|
|
22
|
+
background-color: white;
|
|
23
|
+
overflow: hidden !important;
|
|
24
|
+
border: 1px solid rgba(88, 88, 91, 0.24);
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
padding: 0;
|
|
28
|
+
margin: 0;
|
|
29
|
+
order: 2;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.pagination>* {
|
|
33
|
+
width: 3.4rem !important;
|
|
34
|
+
height: 100% !important;
|
|
35
|
+
background-color: white;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
padding: 0;
|
|
38
|
+
border: 0;
|
|
39
|
+
color: #1C2430;
|
|
40
|
+
font: 400 1.4rem/normal 'Inter' !important;
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
list-style: none;
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.pagination>*>* {
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 100%;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.pagination>*.active {
|
|
55
|
+
background-color: var(--infor-color) !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.pagination>*.active>* {
|
|
59
|
+
color: white;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.pagination>*:not(:last-child) {
|
|
63
|
+
border-right: 1px solid rgba(88, 88, 91, 0.24);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.pagination>:first-child,
|
|
67
|
+
.pagination>:last-child {
|
|
68
|
+
width: 8rem !important;
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { CSSProperties } from "react";
|
|
2
|
+
import ReactPaginate from "react-paginate";
|
|
3
|
+
import './pagination.css';
|
|
4
|
+
import { Select1, Text } from "../export-component";
|
|
5
|
+
|
|
6
|
+
export function Pagination({ currentPage, itemPerPage, totalItem, onChangePage, hiddenPageSize = false, hiddenTotal = false, style }: { currentPage: number, itemPerPage: number, totalItem: number, onChangePage: Function, hiddenPageSize: boolean, hiddenTotal: boolean, style: CSSProperties }) {
|
|
7
|
+
if (currentPage > 1 && (totalItem === 0 || (Math.floor(totalItem / itemPerPage) + (totalItem % itemPerPage === 0 ? 0 : 1)) < currentPage)) {
|
|
8
|
+
onChangePage(1, itemPerPage);
|
|
9
|
+
return <div></div>;
|
|
10
|
+
}
|
|
11
|
+
if (totalItem > 0) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="row custom-pagination" style={style}>
|
|
14
|
+
{hiddenTotal ? null : <Text className="regular2">
|
|
15
|
+
Hiển thị {itemPerPage * (currentPage - 1) + 1}-{((itemPerPage * (currentPage - 1) + itemPerPage) > totalItem) ? totalItem : (itemPerPage * (currentPage - 1) + itemPerPage)} trong tổng số {totalItem} bản ghi
|
|
16
|
+
</Text>}
|
|
17
|
+
<div className="row ">
|
|
18
|
+
{hiddenPageSize ? null : <div className="row items-per-page-container" >
|
|
19
|
+
<Text className="regular2">Items/page</Text>
|
|
20
|
+
<div className="row">
|
|
21
|
+
<Select1
|
|
22
|
+
searchPlaceholder={'Search'}
|
|
23
|
+
style={{ width: '8.6rem' }}
|
|
24
|
+
placeholder={itemPerPage.toString()}
|
|
25
|
+
options={[10, 20, 50, 100, 200].map((item, _) => { return { id: item, name: item } })}
|
|
26
|
+
onChange={(ev: any) => {
|
|
27
|
+
onChangePage(currentPage, isNaN(parseInt(ev.id)) ? itemPerPage : parseInt(ev.id));
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</div>}
|
|
32
|
+
<ReactPaginate
|
|
33
|
+
breakLabel="..."
|
|
34
|
+
nextLabel="Next"
|
|
35
|
+
onPageChange={(ev) => {
|
|
36
|
+
onChangePage(ev.selected + 1, itemPerPage);
|
|
37
|
+
}}
|
|
38
|
+
forcePage={currentPage - 1}
|
|
39
|
+
// initialPage={currentPage - 1}
|
|
40
|
+
pageCount={Math.ceil(totalItem / itemPerPage)}
|
|
41
|
+
previousLabel="Previous"
|
|
42
|
+
containerClassName="pagination row"
|
|
43
|
+
pageClassName=""
|
|
44
|
+
pageLinkClassName="nav-link"
|
|
45
|
+
previousClassName="nav-link"
|
|
46
|
+
previousLinkClassName="nav-link"
|
|
47
|
+
nextClassName="nav-link regular2"
|
|
48
|
+
nextLinkClassName="nav-link"
|
|
49
|
+
activeClassName="active"
|
|
50
|
+
hrefBuilder={(pageIndex: number) =>
|
|
51
|
+
pageIndex >= 1 && pageIndex <= Math.ceil(totalItem / itemPerPage) ? `/page/${pageIndex}` : '#'
|
|
52
|
+
}
|
|
53
|
+
renderOnZeroPageCount={null}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
return (
|
|
60
|
+
<div >
|
|
61
|
+
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.popup-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
z-index: 10;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.popup-container {
|
|
15
|
+
position: relative;
|
|
16
|
+
background-color: #ffffff;
|
|
17
|
+
border-radius: 0.8rem;
|
|
18
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
19
|
+
overflow: visible;
|
|
20
|
+
max-width: 86%;
|
|
21
|
+
max-height: 80%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.popup-container .view-form {
|
|
25
|
+
border-radius: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.popup-header {
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: start;
|
|
31
|
+
padding: 1.6rem 2.4rem;
|
|
32
|
+
border-bottom: 1px solid rgba(0, 53, 128, 0.08);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.popup-footer {
|
|
36
|
+
column-gap: 0.8rem;
|
|
37
|
+
justify-content: end;
|
|
38
|
+
padding: 1.2rem 2.4rem;
|
|
39
|
+
box-shadow: 0 -1px 6px 0 #2D32390F;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.popup-footer>.popup-action {
|
|
43
|
+
height: 4rem;
|
|
44
|
+
padding: 0 1.6rem;
|
|
45
|
+
border-radius: 0.8rem;
|
|
46
|
+
background-color: #f2f5f8;
|
|
47
|
+
align-items: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.popup-footer>.popup-submit {
|
|
51
|
+
color: #ffffff;
|
|
52
|
+
background-color: var(--infor-color);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.popup-container>.popup-close-btn {
|
|
56
|
+
position: absolute;
|
|
57
|
+
right: 0;
|
|
58
|
+
top: 0;
|
|
59
|
+
transform: translate(50%, -50%);
|
|
60
|
+
width: 3.2rem;
|
|
61
|
+
height: 3.2rem;
|
|
62
|
+
padding: 0.6rem;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
background-color: #ffffff;
|
|
67
|
+
box-shadow: 0px 1px 6px 0px #2d32390f;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.popup-body {
|
|
71
|
+
width: 100%;
|
|
72
|
+
max-height: 100%;
|
|
73
|
+
flex: 1;
|
|
74
|
+
overflow: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.popup-overlay.hidden-overlay {
|
|
78
|
+
background-color: rgba(0, 0, 0, 0.01);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.popup-overlay.hidden-overlay>.popup-container {
|
|
82
|
+
position: absolute;
|
|
83
|
+
width: max-content;
|
|
84
|
+
height: max-content;
|
|
85
|
+
background-color: transparent;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.popup-overlay.hidden-overlay>.popup-container .popup-close-btn {
|
|
89
|
+
display: none !important;
|
|
90
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom'
|
|
3
|
+
import './popup.css'
|
|
4
|
+
|
|
5
|
+
interface PopupState {
|
|
6
|
+
readonly open?: boolean,
|
|
7
|
+
heading?: ReactNode,
|
|
8
|
+
content?: ReactNode,
|
|
9
|
+
footer?: ReactNode,
|
|
10
|
+
clickOverlayClosePopup?: boolean,
|
|
11
|
+
style?: CSSProperties,
|
|
12
|
+
hideButtonClose?: boolean,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const showPopup = ({ ref, heading, content, footer, clickOverlayClosePopup, style, hideButtonClose }: {
|
|
16
|
+
ref: React.MutableRefObject<Popup | undefined>,
|
|
17
|
+
heading?: ReactNode,
|
|
18
|
+
content?: ReactNode,
|
|
19
|
+
footer?: ReactNode,
|
|
20
|
+
clickOverlayClosePopup?: boolean,
|
|
21
|
+
style?: CSSProperties,
|
|
22
|
+
hideButtonClose?: boolean
|
|
23
|
+
}) => {
|
|
24
|
+
ref?.current?.onOpen({
|
|
25
|
+
heading: heading,
|
|
26
|
+
content: content,
|
|
27
|
+
footer: footer,
|
|
28
|
+
clickOverlayClosePopup: clickOverlayClosePopup,
|
|
29
|
+
style: style,
|
|
30
|
+
hideButtonClose: hideButtonClose
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const closePopup = (ref: React.MutableRefObject<Popup>) => {
|
|
35
|
+
ref.current.onClose()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Popup extends React.Component<Object, PopupState> {
|
|
39
|
+
private ref: React.RefObject<HTMLDivElement>;
|
|
40
|
+
constructor(props: Object | Readonly<Object>) {
|
|
41
|
+
super(props);
|
|
42
|
+
this.ref = React.createRef();
|
|
43
|
+
}
|
|
44
|
+
state: Readonly<PopupState> = {
|
|
45
|
+
open: false,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onOpen(data: PopupState) {
|
|
49
|
+
this.setState({ open: true, ...data })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onClose() {
|
|
53
|
+
this.setState({ open: false })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
componentDidUpdate(prevProps: Readonly<Object>, prevState: Readonly<PopupState>) {
|
|
57
|
+
if (prevState.open !== this.state.open && this.state.open && this.state.style) {
|
|
58
|
+
const thisPopupRect = this.ref.current?.getBoundingClientRect()
|
|
59
|
+
if (thisPopupRect) {
|
|
60
|
+
let style: CSSProperties | undefined;
|
|
61
|
+
if (thisPopupRect.right > document.body.offsetWidth) {
|
|
62
|
+
style = { ...this.state.style, right: '0.4rem' }
|
|
63
|
+
delete style.left
|
|
64
|
+
}
|
|
65
|
+
if (thisPopupRect.bottom > document.body.offsetHeight) {
|
|
66
|
+
style = style ? {
|
|
67
|
+
...style,
|
|
68
|
+
bottom: '0.4rem'
|
|
69
|
+
} : {
|
|
70
|
+
...this.state.style,
|
|
71
|
+
bottom: '0.4rem'
|
|
72
|
+
}
|
|
73
|
+
delete style.top
|
|
74
|
+
}
|
|
75
|
+
if (style) this.setState({ ...this.state, style: style })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render() {
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
{this.state.open &&
|
|
84
|
+
ReactDOM.createPortal(
|
|
85
|
+
<div className={`popup-overlay ${this.state.clickOverlayClosePopup ? 'hidden-overlay' : ''}`} onClick={this.state.clickOverlayClosePopup ? (ev) => {
|
|
86
|
+
if ((ev.target as HTMLElement).classList.contains('popup-overlay'))
|
|
87
|
+
this.onClose()
|
|
88
|
+
} : undefined}>
|
|
89
|
+
<div ref={this.ref} className='popup-container col' onClick={e => e.stopPropagation()} style={this.state.style} >
|
|
90
|
+
{this.state.heading}
|
|
91
|
+
{this.state.content}
|
|
92
|
+
{this.state.footer}
|
|
93
|
+
{this.state.hideButtonClose ? null : <button type='button' onClick={() => this.onClose()} className='popup-close-btn row' >
|
|
94
|
+
<svg width='100%' height='100%' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg' style={{ width: '2rem', height: '2rem' }} >
|
|
95
|
+
<path fillRule='evenodd' clipRule='evenodd' d='M16.4223 4.7559C16.7477 4.43047 16.7477 3.90283 16.4223 3.57739C16.0968 3.25195 15.5692 3.25195 15.2438 3.57739L9.99967 8.82147L4.7556 3.57739C4.43016 3.25195 3.90252 3.25195 3.57709 3.57739C3.25165 3.90283 3.25165 4.43047 3.57709 4.7559L8.82116 9.99998L3.57709 15.2441C3.25165 15.5695 3.25165 16.0971 3.57709 16.4226C3.90252 16.748 4.43016 16.748 4.7556 16.4226L9.99967 11.1785L15.2438 16.4226C15.5692 16.748 16.0968 16.748 16.4223 16.4226C16.7477 16.0971 16.7477 15.5695 16.4223 15.2441L11.1782 9.99998L16.4223 4.7559Z' fill='#00204D' fillOpacity={0.6} />
|
|
96
|
+
</svg>
|
|
97
|
+
</button>}
|
|
98
|
+
</div>
|
|
99
|
+
</div>,
|
|
100
|
+
document.body
|
|
101
|
+
)}
|
|
102
|
+
</>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
.progress-bar-container {
|
|
2
|
+
row-gap: 1.2rem;
|
|
3
|
+
width: 38.2rem;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.progress-bar-container>.progress-bar-title {
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.progress-bar-container>.progress-bar-title>.heading-text {
|
|
12
|
+
font: 600 1.4rem/2.2rem 'Inter';
|
|
13
|
+
color: #00204D;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.progress-bar-container>.progress-bar-tile {
|
|
17
|
+
column-gap: 1.6rem;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 2.2rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.progress-bar-container>.progress-bar-tile>.progress-bar-value {
|
|
23
|
+
border-radius: 10rem;
|
|
24
|
+
width: 100%;
|
|
25
|
+
flex: 1;
|
|
26
|
+
height: 0.6rem;
|
|
27
|
+
position: relative;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
background-color: var(--full-color);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.progress-bar-container>.progress-bar-tile>.progress-bar-value::after {
|
|
33
|
+
content: '';
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 0;
|
|
36
|
+
left: 0;
|
|
37
|
+
width: var(--percent);
|
|
38
|
+
height: 0.6rem;
|
|
39
|
+
border-radius: 10rem;
|
|
40
|
+
background-color: var(--percent-color);
|
|
41
|
+
transition: width ease-in-out 1s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.progress-bar-container>.progress-bar-tile>.text-value {
|
|
45
|
+
font: 400 1.4rem/2.2rem 'Inter';
|
|
46
|
+
color: #00204DE5;
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode, useState } from 'react'
|
|
2
|
+
import './progress-bar.css'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
5
|
+
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
|
6
|
+
import { ComponentStatus, getStatusIcon } from '../export-component'
|
|
7
|
+
|
|
8
|
+
export function ProgressBar({ status = ComponentStatus.INFOR, percent = 100, titleText, title, hideTitle = false, progressBarOnly = false, fullColor = 'var(--background)', percentColor = 'var(--infor-color)', style, progressBarStyle }: {
|
|
9
|
+
percent: number,
|
|
10
|
+
titleText?: string,
|
|
11
|
+
title?: ReactNode,
|
|
12
|
+
hideTitle: boolean,
|
|
13
|
+
progressBarOnly: boolean,
|
|
14
|
+
fullColor: string,
|
|
15
|
+
percentColor: string,
|
|
16
|
+
style?: CSSProperties,
|
|
17
|
+
status: ComponentStatus,
|
|
18
|
+
progressBarStyle?: CSSProperties
|
|
19
|
+
}) {
|
|
20
|
+
const [openDetails, setOpenDetails] = useState(true)
|
|
21
|
+
|
|
22
|
+
return <div className="progress-bar-container col" style={style ? { padding: progressBarOnly ? '0' : '1.6rem 2.4rem', ...style } : { padding: progressBarOnly ? '0' : '1.6rem 2.4rem' }}>
|
|
23
|
+
{(hideTitle || progressBarOnly) ? null : (title ?? <div className="progress-bar-title row">
|
|
24
|
+
<div className="heading-text">{titleText}</div>
|
|
25
|
+
<button type="button" className="suffix-action" onClick={() => { setOpenDetails(!openDetails) }}><FontAwesomeIcon icon={openDetails ? faChevronDown : faChevronUp} /></button>
|
|
26
|
+
</div>)}
|
|
27
|
+
{openDetails ? <div className='progress-bar-tile row' >
|
|
28
|
+
<div className="progress-bar-value" style={{ '--percent-color': percentColor, '--full-color': fullColor, '--percent': `${percent}%`, ...(progressBarStyle ?? {}) } as CSSProperties}></div>
|
|
29
|
+
{progressBarOnly || status === ComponentStatus.INFOR ? null : <div className='status-icon'>{getStatusIcon(status)}</div>}
|
|
30
|
+
{progressBarOnly ? null : <div className='text-value'>{percent}/100</div>}
|
|
31
|
+
</div> : null}
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
}
|