@lowentry/mui 1.0.1
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/.github/workflows/on-push-to-master.yaml +30 -0
- package/LeMuiUtils.js +52 -0
- package/README.md +22 -0
- package/index.js +10 -0
- package/package.json +34 -0
- package/widgets/DatePicker.css +32 -0
- package/widgets/DatePicker.jsx +110 -0
- package/widgets/Dialog.jsx +28 -0
- package/widgets/LoadingSpinner.css +9 -0
- package/widgets/LoadingSpinner.jsx +50 -0
- package/widgets/MenuButton.jsx +51 -0
- package/widgets/MuiRoot.css +13 -0
- package/widgets/MuiRoot.jsx +21 -0
- package/widgets/NumericTextField.jsx +116 -0
- package/widgets/RemovableNumericTextField.jsx +40 -0
- package/widgets/RemovableTextField.jsx +47 -0
- package/widgets/Submittable.jsx +39 -0
- package/widgets/TextField.css +4 -0
- package/widgets/TextField.jsx +29 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: on push to master
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.event_name }}
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
name: "Increase version and deploy"
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 'lts/*'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
#- run: npm ci
|
|
23
|
+
- run: npm test
|
|
24
|
+
- run: git config --global user.name "github-actions[bot]"
|
|
25
|
+
- run: git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
26
|
+
- run: npm version patch -m "Upgrade to %s [skip ci]"
|
|
27
|
+
- run: git push && git push --tags
|
|
28
|
+
- run: npm publish --access public
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LeMuiUtils.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {LeUtils, STRING, INT_LAX} from '@lowentry/utils';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const HIDDEN_CHAR = '\u200B';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const LeMuiUtils = {
|
|
8
|
+
onSelectEnsureMinimumOffset:
|
|
9
|
+
(charactersCount) =>
|
|
10
|
+
{
|
|
11
|
+
charactersCount = Math.max(0, INT_LAX(charactersCount));
|
|
12
|
+
if(charactersCount <= 0)
|
|
13
|
+
{
|
|
14
|
+
return () =>
|
|
15
|
+
{
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (event) =>
|
|
20
|
+
{
|
|
21
|
+
if(event && event.target)
|
|
22
|
+
{
|
|
23
|
+
if(event.target.selectionEnd < charactersCount)
|
|
24
|
+
{
|
|
25
|
+
event.target.setSelectionRange(charactersCount, charactersCount);
|
|
26
|
+
}
|
|
27
|
+
else if(event.target.selectionStart < charactersCount)
|
|
28
|
+
{
|
|
29
|
+
event.target.setSelectionRange(charactersCount, event.target.selectionEnd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
prependHiddenChar:
|
|
36
|
+
(string) =>
|
|
37
|
+
{
|
|
38
|
+
string = STRING(string);
|
|
39
|
+
if(string.startsWith(HIDDEN_CHAR))
|
|
40
|
+
{
|
|
41
|
+
return string;
|
|
42
|
+
}
|
|
43
|
+
return HIDDEN_CHAR + string;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
purgePrependedHiddenChar:
|
|
47
|
+
(string) =>
|
|
48
|
+
{
|
|
49
|
+
string = STRING(string);
|
|
50
|
+
return LeUtils.trimStart(string, ['\u200B', ' ', '\t', '\r', '\n']);
|
|
51
|
+
},
|
|
52
|
+
};
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @lowentry/firebase
|
|
2
|
+
|
|
3
|
+
Simplifies the use of Material UI in your React project.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
|
|
8
|
+
This plugin adds utility classes to make it easier to build user interfaces with Material UI.
|
|
9
|
+
|
|
10
|
+
Think about things such as:
|
|
11
|
+
|
|
12
|
+
- numeric textfields (that automatically format the input as a number, without the constrictions that type="number" has)
|
|
13
|
+
- removable textfields (textfields that call a onRemove() callback when the user presses the backspace key while the textfield is empty)
|
|
14
|
+
- loading spinners (that maintain their rotation through state changes, including when the component is unmounted and remounted, or hidden/shown)
|
|
15
|
+
- etc.
|
|
16
|
+
|
|
17
|
+
This just makes it easier to build consistent and user-friendly interfaces.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Final words
|
|
21
|
+
|
|
22
|
+
I hope this plugin will be useful to you. If you have any questions or suggestions, please feel free to get in touch at [LowEntry.com](https://lowentry.com/).
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {DatePicker} from './widgets/DatePicker.jsx';
|
|
2
|
+
export {Dialog} from './widgets/Dialog.jsx';
|
|
3
|
+
export {LoadingSpinner} from './widgets/LoadingSpinner.jsx';
|
|
4
|
+
export {MenuButton} from './widgets/MenuButton.jsx';
|
|
5
|
+
export {MuiRoot} from './widgets/MuiRoot.jsx';
|
|
6
|
+
export {NumericTextField} from './widgets/NumericTextField.jsx';
|
|
7
|
+
export {RemovableNumericTextField} from './widgets/RemovableNumericTextField.jsx';
|
|
8
|
+
export {RemovableTextField} from './widgets/RemovableTextField.jsx';
|
|
9
|
+
export {Submittable} from './widgets/Submittable.jsx';
|
|
10
|
+
export {TextField} from './widgets/TextField.jsx';
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lowentry/mui",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Provides utilities for Material UI.",
|
|
7
|
+
"author": "Low Entry",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"materialui",
|
|
11
|
+
"mui",
|
|
12
|
+
"plugin",
|
|
13
|
+
"utility",
|
|
14
|
+
"utilities"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/LowEntry/lowentry-js-mui",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/LowEntry/lowentry-js-mui.git"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --check index.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@emotion/react": "*",
|
|
26
|
+
"@emotion/styled": "*",
|
|
27
|
+
"@lowentry/react-redux": "^1",
|
|
28
|
+
"@mui/icons-material": "*",
|
|
29
|
+
"@mui/lab": "*",
|
|
30
|
+
"@mui/material": "^5",
|
|
31
|
+
"@mui/x-date-pickers": "^7",
|
|
32
|
+
"dayjs": "^1"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.lowentry-mui--date-picker
|
|
2
|
+
{
|
|
3
|
+
padding: 0.5rem;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.lowentry-mui--date-picker .lowentry-mui--date-picker--arrow-button
|
|
7
|
+
{
|
|
8
|
+
min-width: 0;
|
|
9
|
+
padding: 6px 12px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lowentry-mui--date-picker .lowentry-mui--date-picker--textfield
|
|
13
|
+
{
|
|
14
|
+
max-width: 150px;
|
|
15
|
+
cursor: pointer !important;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.lowentry-mui--date-picker .lowentry-mui--date-picker--textfield .MuiInputBase-input
|
|
19
|
+
{
|
|
20
|
+
text-align: center;
|
|
21
|
+
padding: 10px 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.lowentry-mui--date-picker .lowentry-mui--date-picker--textfield .MuiOutlinedInput-notchedOutline
|
|
25
|
+
{
|
|
26
|
+
border: 0 !important;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.lowentry-mui--date-picker .lowentry-mui--date-picker--textfield *
|
|
30
|
+
{
|
|
31
|
+
cursor: pointer !important;
|
|
32
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {IS_ARRAY} from '@lowentry/utils';
|
|
3
|
+
import Dayjs from 'dayjs';
|
|
4
|
+
import {Button, Stack} from '@mui/material';
|
|
5
|
+
import {DatePicker as MuiDatePicker} from '@mui/x-date-pickers';
|
|
6
|
+
import {TextField} from './TextField.jsx';
|
|
7
|
+
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
|
8
|
+
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
|
9
|
+
import './DatePicker.css';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const DatePickerTextField = LeRed.memo((props) =>
|
|
13
|
+
{
|
|
14
|
+
const onClick = LeRed.useCallback(() =>
|
|
15
|
+
{
|
|
16
|
+
if(props?.ownerState?.open)
|
|
17
|
+
{
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
let propsLoop = props?.InputProps?.endAdornment?.props;
|
|
21
|
+
while(propsLoop)
|
|
22
|
+
{
|
|
23
|
+
if(propsLoop.onClick)
|
|
24
|
+
{
|
|
25
|
+
return propsLoop.onClick;
|
|
26
|
+
}
|
|
27
|
+
if(propsLoop?.children)
|
|
28
|
+
{
|
|
29
|
+
propsLoop = propsLoop.children;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if(IS_ARRAY(propsLoop))
|
|
33
|
+
{
|
|
34
|
+
let continueLoop = false;
|
|
35
|
+
for(const propsLoopItem of propsLoop)
|
|
36
|
+
{
|
|
37
|
+
if(propsLoopItem?.props)
|
|
38
|
+
{
|
|
39
|
+
propsLoop = propsLoopItem.props;
|
|
40
|
+
continueLoop = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if(continueLoop)
|
|
44
|
+
{
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
propsLoop = propsLoop?.props;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}, [props?.ownerState?.open, props?.InputProps?.endAdornment?.props]);
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const onSelect = LeRed.useCallback((event) =>
|
|
55
|
+
{
|
|
56
|
+
event.target.selectionStart = event.target.selectionEnd;
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
return (<>
|
|
62
|
+
<Button variant="outlined" onClick={onClick}>
|
|
63
|
+
<TextField {...props} className={'lowentry-mui--date-picker--textfield ' + (props.className ?? '')} variant="outlined" value={Dayjs(props.value).format(props?.dateFormat)} InputProps={{...(props.InputProps ?? {}), readOnly:true, endAdornment:null, onSelect:onSelect, onSelectCapture:onSelect, onMouseDown:onSelect, onTouchStart:onSelect, onTouchMove:onSelect}}/>
|
|
64
|
+
</Button>
|
|
65
|
+
</>);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export const DatePicker = LeRed.memo(({value, dateFormat, onChange, className, children, ...props}) =>
|
|
70
|
+
{
|
|
71
|
+
if(!dateFormat)
|
|
72
|
+
{
|
|
73
|
+
dateFormat = 'ddd, D MMM YYYY';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [datepickerOpen, openDatepicker, closeDatepicker] = LeRed.useHistoryState(false);
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
const onChanged = LeRed.useCallback((...args) =>
|
|
80
|
+
{
|
|
81
|
+
if(onChange)
|
|
82
|
+
{
|
|
83
|
+
onChange(...args);
|
|
84
|
+
}
|
|
85
|
+
}, [onChange]);
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return (<>
|
|
89
|
+
<Stack className={'lowentry-mui--date-picker ' + (className ?? '')} direction="row" justifyContent="space-between" spacing={1} {...props}>
|
|
90
|
+
<Button className="lowentry-mui--date-picker--arrow-button" variant="text" color="primary" onClick={() => onChanged(Dayjs(value).subtract(1, 'day'))}><ArrowBackIosNewIcon/></Button>
|
|
91
|
+
<MuiDatePicker
|
|
92
|
+
open={datepickerOpen}
|
|
93
|
+
onOpen={openDatepicker}
|
|
94
|
+
onClose={closeDatepicker}
|
|
95
|
+
showDaysOutsideCurrentMonth={true}
|
|
96
|
+
views={['day']}
|
|
97
|
+
format="YYYY-MM-DD"
|
|
98
|
+
label=""
|
|
99
|
+
isRequired={true}
|
|
100
|
+
value={value}
|
|
101
|
+
onChange={onChanged}
|
|
102
|
+
slots={{
|
|
103
|
+
textField:(props) => LeRed.createElement(DatePickerTextField, {dateFormat, ...props}),
|
|
104
|
+
toolbar: (props) => null,
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
<Button className="lowentry-mui--date-picker--arrow-button" variant="text" color="primary" onClick={() => onChanged(Dayjs(value).add(1, 'day'))}><ArrowForwardIosIcon/></Button>
|
|
108
|
+
</Stack>
|
|
109
|
+
</>);
|
|
110
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {STRING} from '@lowentry/utils';
|
|
3
|
+
import {Dialog} from '@mui/material';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const Dialog = LeRed.memo(({onClose, children, ...props}) =>
|
|
7
|
+
{
|
|
8
|
+
const onClosed = LeRed.useCallback((event, reason) =>
|
|
9
|
+
{
|
|
10
|
+
if(!STRING(reason).toLowerCase().includes('escape'))
|
|
11
|
+
{
|
|
12
|
+
// prevent closing when clicking on the backdrop, only allow escape-key closing
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if(onClose)
|
|
17
|
+
{
|
|
18
|
+
onClose(event, reason);
|
|
19
|
+
}
|
|
20
|
+
}, [onClose]);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
return (<>
|
|
24
|
+
<Dialog onClose={onClosed} {...props}>
|
|
25
|
+
{children}
|
|
26
|
+
</Dialog>
|
|
27
|
+
</>);
|
|
28
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeRed} from '@lowentry/react-redux';
|
|
3
|
+
import {Backdrop, CircularProgress} from '@mui/material';
|
|
4
|
+
import './LoadingSpinner.css';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
let loadingSpinnerCount = {};
|
|
8
|
+
let loadingSpinnerCountNonTransparent = 0;
|
|
9
|
+
|
|
10
|
+
const LoadingSpinnerGrab = ({type}) =>
|
|
11
|
+
{
|
|
12
|
+
loadingSpinnerCount[type] = (loadingSpinnerCount[type] || 0) + 1;
|
|
13
|
+
if(loadingSpinnerCount[type] === 1)
|
|
14
|
+
{
|
|
15
|
+
LeRed.trigger('lowentry-mui--loading-spinner--' + type);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const LoadingSpinnerRelease = ({type}) =>
|
|
20
|
+
{
|
|
21
|
+
loadingSpinnerCount[type] = (loadingSpinnerCount[type] || 1) - 1;
|
|
22
|
+
if(loadingSpinnerCount[type] === 0)
|
|
23
|
+
{
|
|
24
|
+
LeRed.trigger('lowentry-mui--loading-spinner--' + type);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export const LoadingSpinner = LeRed.memo(({type}) =>
|
|
30
|
+
{
|
|
31
|
+
LeRed.useEffect(() =>
|
|
32
|
+
{
|
|
33
|
+
LoadingSpinnerGrab({type});
|
|
34
|
+
return () => LoadingSpinnerRelease({type});
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export const LoadingSpinnerWidget = LeRed.memo(({type, className, sx, children, ...props}) =>
|
|
42
|
+
{
|
|
43
|
+
LeRed.useTriggerable('lowentry-mui--loading-spinner--' + type);
|
|
44
|
+
|
|
45
|
+
return (<>
|
|
46
|
+
<Backdrop className={'lowentry-mui--loading-spinner lowentry-mui--loading-spinner--' + type + ' ' + (className ?? '')} sx={{zIndex:(theme) => theme.zIndex.drawer + 1, ...(sx ?? {})}} open={(loadingSpinnerCount[type] || 0) > 0} {...props}>
|
|
47
|
+
<CircularProgress color="inherit" size="min(120px,30vw)"/>
|
|
48
|
+
</Backdrop>
|
|
49
|
+
</>);
|
|
50
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeUtils, ARRAY} from '@lowentry/utils';
|
|
3
|
+
import {Button, Menu} from '@mui/material';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const MenuButton = LeRed.memo(({icon, className, ref, onClick, onClose, children, ...props}) =>
|
|
7
|
+
{
|
|
8
|
+
const buttonRef = LeRed.useRef();
|
|
9
|
+
const [open, setOpen] = LeRed.useState(false);
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const onClicked = LeRed.useCallback((event) =>
|
|
13
|
+
{
|
|
14
|
+
setOpen(true);
|
|
15
|
+
|
|
16
|
+
if(onClick)
|
|
17
|
+
{
|
|
18
|
+
onClick(event);
|
|
19
|
+
}
|
|
20
|
+
}, [onClick]);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const onClosed = LeRed.useCallback((event) =>
|
|
24
|
+
{
|
|
25
|
+
setOpen(false);
|
|
26
|
+
|
|
27
|
+
if(onClose)
|
|
28
|
+
{
|
|
29
|
+
onClose(event);
|
|
30
|
+
}
|
|
31
|
+
}, [onClose]);
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
return (<>
|
|
35
|
+
<Button ref={LeRed.mergeRefs(buttonRef, ref)} className={'lowentry-mui--menu-button allow-mobile-hover ' + (className ?? '')} variant="text" color="primary" onClick={onClicked} {...props}>{icon}</Button>
|
|
36
|
+
<Menu anchorEl={buttonRef.current} open={open} onClose={onClosed}>
|
|
37
|
+
{LeUtils.mapToArray(ARRAY(children), (child, index) => !!child && LeRed.cloneElement(child, {
|
|
38
|
+
key: index,
|
|
39
|
+
onClick: () =>
|
|
40
|
+
{
|
|
41
|
+
setOpen(false);
|
|
42
|
+
if(child?.props?.onClick)
|
|
43
|
+
{
|
|
44
|
+
child.props.onClick();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
disabled:(typeof child?.props?.disabled !== 'undefined') ? child.props.disabled : !child?.props?.onClick,
|
|
48
|
+
}))}
|
|
49
|
+
</Menu>
|
|
50
|
+
</>);
|
|
51
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.lowentry-mui--mui-root
|
|
2
|
+
{
|
|
3
|
+
width: 100%;
|
|
4
|
+
min-height: 100dvh;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
@media not (hover: hover), not (pointer: fine)
|
|
8
|
+
{
|
|
9
|
+
.lowentry-mui--mui-root .MuiButtonBase-root:not(.allow-mobile-hover):hover
|
|
10
|
+
{
|
|
11
|
+
background-color: rgba(255, 255, 255, 0) !important;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeRed} from '@lowentry/react-redux';
|
|
3
|
+
import {CssBaseline, ThemeProvider} from '@mui/material';
|
|
4
|
+
import {LocalizationProvider} from '@mui/x-date-pickers';
|
|
5
|
+
import {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';
|
|
6
|
+
import './MuiRoot.css';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const MuiRoot = LeRed.memo(({theme, className, children, ...props}) =>
|
|
10
|
+
{
|
|
11
|
+
return (<>
|
|
12
|
+
<CssBaseline/>
|
|
13
|
+
<ThemeProvider theme={theme}>
|
|
14
|
+
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
15
|
+
<div className={'lowentry-mui--mui-root ' + (className ?? '')} {...props}>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
</LocalizationProvider>
|
|
19
|
+
</ThemeProvider>
|
|
20
|
+
</>);
|
|
21
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeRed} from '@lowentry/react-redux';
|
|
3
|
+
import {LeUtils, FLOAT_LAX, INT_LAX_ANY} from '@lowentry/utils';
|
|
4
|
+
import {TextField} from './TextField.jsx';
|
|
5
|
+
import {LeMuiUtils} from '../LeMuiUtils.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const NumericTextField = LeRed.memo(({decimals, allowZero, allowNegative, value, onChange, onRenderValue, className, inputProps, children, ...props}) =>
|
|
9
|
+
{
|
|
10
|
+
if(typeof allowZero === 'undefined')
|
|
11
|
+
{
|
|
12
|
+
allowZero = true;
|
|
13
|
+
}
|
|
14
|
+
if(typeof allowNegative === 'undefined')
|
|
15
|
+
{
|
|
16
|
+
allowNegative = true;
|
|
17
|
+
}
|
|
18
|
+
decimals = INT_LAX_ANY(decimals, 2);
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const getVisualValue = (value) =>
|
|
22
|
+
{
|
|
23
|
+
let text = FLOAT_LAX(LeMuiUtils.purgePrependedHiddenChar(value));
|
|
24
|
+
if(!allowNegative)
|
|
25
|
+
{
|
|
26
|
+
text = Math.abs(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
text = text.toFixed(decimals); // rounds it
|
|
30
|
+
text = LeUtils.trimEnd(LeUtils.trimEnd(text, '0'), '.');
|
|
31
|
+
if(!allowZero && (text === '0'))
|
|
32
|
+
{
|
|
33
|
+
text = '';
|
|
34
|
+
}
|
|
35
|
+
return text;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const [visualValue, setVisualValue] = LeRed.useState(getVisualValue(value));
|
|
40
|
+
|
|
41
|
+
LeRed.useEffect(() =>
|
|
42
|
+
{
|
|
43
|
+
setVisualValue(getVisualValue(value));
|
|
44
|
+
}, [value]);
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const onChanged = LeRed.useCallback((event) =>
|
|
48
|
+
{
|
|
49
|
+
const originalTargetValue = event.target.value;
|
|
50
|
+
const targetValue = LeMuiUtils.purgePrependedHiddenChar(originalTargetValue);
|
|
51
|
+
|
|
52
|
+
let text = targetValue;
|
|
53
|
+
let val = 0;
|
|
54
|
+
|
|
55
|
+
{// visual >>
|
|
56
|
+
const minus = text.includes('-');
|
|
57
|
+
text = text.replace(',', '.').replace(/[^0-9.]/g, '');
|
|
58
|
+
if(text !== '')
|
|
59
|
+
{
|
|
60
|
+
val = Math.abs(FLOAT_LAX(text));
|
|
61
|
+
if(allowNegative && minus)
|
|
62
|
+
{
|
|
63
|
+
val = -val;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let stringVal = val.toFixed(decimals + 1); // prevents rounding (by adding an extra digit and then cutting it off)
|
|
67
|
+
stringVal = stringVal.substring(0, stringVal.length - 1);
|
|
68
|
+
|
|
69
|
+
if(!text.includes('.') || (decimals <= 0))
|
|
70
|
+
{
|
|
71
|
+
text = stringVal.split('.')[0];
|
|
72
|
+
}
|
|
73
|
+
else if(text.endsWith('.'))
|
|
74
|
+
{
|
|
75
|
+
text = stringVal.split('.')[0] + '.';
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
text = stringVal.substring(0, stringVal.length - Math.max(0, decimals - text.split('.')[1].length));
|
|
80
|
+
}
|
|
81
|
+
setVisualValue(text);
|
|
82
|
+
}
|
|
83
|
+
}// visual <<
|
|
84
|
+
|
|
85
|
+
if(onChange)
|
|
86
|
+
{
|
|
87
|
+
if(val !== 0)
|
|
88
|
+
{
|
|
89
|
+
if(decimals > 0)
|
|
90
|
+
{
|
|
91
|
+
val = Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
{
|
|
95
|
+
val = Math.round(val);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const newEvent = {
|
|
100
|
+
...event,
|
|
101
|
+
target:{
|
|
102
|
+
...event.target,
|
|
103
|
+
value: val,
|
|
104
|
+
valueText:text,
|
|
105
|
+
valueRaw: originalTargetValue,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
onChange(newEvent);
|
|
109
|
+
}
|
|
110
|
+
}, [onChange]);
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
return (<>
|
|
114
|
+
<TextField className={'lowentry-mui--numeric-textfield ' + (className ?? '')} type="text" inputProps={{inputMode:'decimal', ...(inputProps ?? {})}} value={!!onRenderValue ? onRenderValue(visualValue) : visualValue} onChange={onChanged} {...props}>{children}</TextField>
|
|
115
|
+
</>);
|
|
116
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeRed} from '@lowentry/react-redux';
|
|
3
|
+
import {LeMuiUtils} from '../LeMuiUtils';
|
|
4
|
+
import {NumericTextField} from './NumericTextField.jsx';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const RemovableNumericTextField = LeRed.memo(({onRemove, onChange, onSelect, className, children, ...props}) =>
|
|
8
|
+
{
|
|
9
|
+
const onChanged = LeRed.useCallback((event) =>
|
|
10
|
+
{
|
|
11
|
+
if(event.target.valueRaw === '')
|
|
12
|
+
{
|
|
13
|
+
if(onRemove)
|
|
14
|
+
{
|
|
15
|
+
onRemove(event);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if(onChange)
|
|
21
|
+
{
|
|
22
|
+
onChange(event);
|
|
23
|
+
}
|
|
24
|
+
}, [onRemove, onChange]);
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const onSelected = LeRed.useCallback((event) =>
|
|
28
|
+
{
|
|
29
|
+
LeMuiUtils.onSelectEnsureMinimumOffset(1)(event);
|
|
30
|
+
if(onSelect)
|
|
31
|
+
{
|
|
32
|
+
onSelect(event);
|
|
33
|
+
}
|
|
34
|
+
}, [onSelect]);
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
return (<>
|
|
38
|
+
<NumericTextField className={'lowentry-mui--removable-textfield lowentry-mui--removable-numeric-textfield ' + (className ?? '')} onRenderValue={LeMuiUtils.prependHiddenChar} onChange={onChanged} onSelect={onSelected}>{children}</NumericTextField>
|
|
39
|
+
</>);
|
|
40
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeRed} from '@lowentry/react-redux';
|
|
3
|
+
import {LeMuiUtils} from '../LeMuiUtils';
|
|
4
|
+
import {TextField} from './TextField.jsx';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const RemovableTextField = LeRed.memo(({className, value, onRemove, onChange, onSelect, children, ...props}) =>
|
|
8
|
+
{
|
|
9
|
+
const onChanged = LeRed.useCallback((event) =>
|
|
10
|
+
{
|
|
11
|
+
if(event.target.value === '')
|
|
12
|
+
{
|
|
13
|
+
if(onRemove)
|
|
14
|
+
{
|
|
15
|
+
onRemove(event);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if(onChange)
|
|
21
|
+
{
|
|
22
|
+
const newEvent = {
|
|
23
|
+
...event,
|
|
24
|
+
target:{
|
|
25
|
+
...event.target,
|
|
26
|
+
value:LeMuiUtils.purgePrependedHiddenChar(event.target.value),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
onChange(newEvent);
|
|
30
|
+
}
|
|
31
|
+
}, [onRemove, onChange]);
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const onSelected = LeRed.useCallback((event) =>
|
|
35
|
+
{
|
|
36
|
+
LeMuiUtils.onSelectEnsureMinimumOffset(1)(event);
|
|
37
|
+
if(onSelect)
|
|
38
|
+
{
|
|
39
|
+
onSelect(event);
|
|
40
|
+
}
|
|
41
|
+
}, [onSelect]);
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
return (<>
|
|
45
|
+
<TextField className={'lowentry-mui--removable-textfield ' + (className ?? '')} value={LeMuiUtils.prependHiddenChar(value)} onChange={onChanged} onSelect={onSelected} {...props}>{children}</TextField>
|
|
46
|
+
</>);
|
|
47
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Stack} from '@mui/material';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const Submittable = LeRed.memo(({onSubmit, disabled, sx, children, ...props}) =>
|
|
6
|
+
{
|
|
7
|
+
const handleSubmit = LeRed.useCallback((event) =>
|
|
8
|
+
{
|
|
9
|
+
try
|
|
10
|
+
{
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
}
|
|
13
|
+
catch(e)
|
|
14
|
+
{
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if(disabled)
|
|
18
|
+
{
|
|
19
|
+
if(!(typeof disabled === 'function') || disabled())
|
|
20
|
+
{
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if(onSubmit)
|
|
26
|
+
{
|
|
27
|
+
onSubmit(event);
|
|
28
|
+
}
|
|
29
|
+
}, [onSubmit, disabled]);
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
return (<>
|
|
33
|
+
<form style={sx ?? {}} onSubmit={handleSubmit}>
|
|
34
|
+
<Stack {...props}>
|
|
35
|
+
{children}
|
|
36
|
+
</Stack>
|
|
37
|
+
</form>
|
|
38
|
+
</>);
|
|
39
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {LeRed} from '@lowentry/react-redux';
|
|
3
|
+
import {TextField as MuiTextField} from '@mui/material';
|
|
4
|
+
import './TextField.css';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const TextField = LeRed.memo(({className, onClick, children, ...props}) =>
|
|
8
|
+
{
|
|
9
|
+
const onClicked = LeRed.useCallback((event) =>
|
|
10
|
+
{
|
|
11
|
+
try
|
|
12
|
+
{
|
|
13
|
+
event.stopPropagation();
|
|
14
|
+
}
|
|
15
|
+
catch(e)
|
|
16
|
+
{
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if(onClick)
|
|
20
|
+
{
|
|
21
|
+
onClick(event);
|
|
22
|
+
}
|
|
23
|
+
}, [onClick]);
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
return (<>
|
|
27
|
+
<MuiTextField className={'lowentry-mui--textfield ' + (className ?? '')} autoComplete="off" onClick={onClicked} {...props}>{children}</MuiTextField>
|
|
28
|
+
</>);
|
|
29
|
+
});
|