@kwiz/fluentui 1.0.74 → 1.0.76
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/npm-publish.yml +24 -24
- package/LICENSE +21 -21
- package/README.md +53 -53
- package/dist/@types/forwardRef.d.ts +0 -0
- package/dist/@types/forwardRef.js +1 -0
- package/dist/@types/forwardRef.js.map +1 -0
- package/dist/controls/error-boundary copy.d.ts +23 -0
- package/dist/controls/error-boundary copy.js +33 -0
- package/dist/controls/error-boundary copy.js.map +1 -0
- package/dist/controls/menu.d.ts +3 -1
- package/dist/controls/menu.js +44 -26
- package/dist/controls/menu.js.map +1 -1
- package/dist/controls/search.js +19 -11
- package/dist/controls/search.js.map +1 -1
- package/dist/controls/svg.js +21 -21
- package/dist/controls/svg.js.map +1 -1
- package/dist/helpers/common.d.ts +4 -0
- package/dist/helpers/common.js +2 -0
- package/dist/helpers/common.js.map +1 -0
- package/dist/helpers/context.d.ts +26 -0
- package/dist/helpers/context.js +15 -0
- package/dist/helpers/context.js.map +1 -0
- package/dist/helpers/drag-drop/exports.d.ts +12 -0
- package/dist/helpers/drag-drop/exports.js +3 -0
- package/dist/helpers/drag-drop/exports.js.map +1 -0
- package/dist/helpers/exports.d.ts +7 -0
- package/dist/helpers/exports.js +8 -0
- package/dist/helpers/exports.js.map +1 -0
- package/dist/helpers/hooks.d.ts +3 -1
- package/dist/helpers/hooks.js +18 -0
- package/dist/helpers/hooks.js.map +1 -1
- package/package.json +85 -84
- package/src/_modules/config.ts +9 -9
- package/src/_modules/constants.ts +3 -3
- package/src/controls/ColorPickerDialog.tsx +83 -83
- package/src/controls/accordion.tsx +62 -62
- package/src/controls/button.tsx +180 -180
- package/src/controls/canvas/CustomEventTargetBase.ts +32 -32
- package/src/controls/canvas/DrawPad.tsx +296 -296
- package/src/controls/canvas/DrawPadManager.ts +694 -694
- package/src/controls/canvas/bezier.ts +109 -109
- package/src/controls/canvas/point.ts +44 -44
- package/src/controls/card-list.tsx +31 -31
- package/src/controls/card.tsx +77 -77
- package/src/controls/centered.tsx +14 -14
- package/src/controls/date.tsx +87 -87
- package/src/controls/diagram-picker.tsx +96 -96
- package/src/controls/divider.tsx +15 -15
- package/src/controls/dropdown.tsx +66 -66
- package/src/controls/error-boundary.tsx +41 -41
- package/src/controls/field-editor.tsx +42 -42
- package/src/controls/file-upload.tsx +155 -155
- package/src/controls/horizontal.tsx +48 -48
- package/src/controls/html-editor/editor.tsx +182 -182
- package/src/controls/index.ts +33 -33
- package/src/controls/input.tsx +160 -160
- package/src/controls/kwizoverflow.tsx +106 -106
- package/src/controls/list.tsx +119 -119
- package/src/controls/loading.tsx +10 -10
- package/src/controls/menu.tsx +195 -173
- package/src/controls/merge-text.tsx +126 -126
- package/src/controls/please-wait.tsx +32 -32
- package/src/controls/progress-bar.tsx +109 -109
- package/src/controls/prompt.tsx +121 -121
- package/src/controls/qrcode.tsx +36 -36
- package/src/controls/search.tsx +71 -61
- package/src/controls/section.tsx +133 -133
- package/src/controls/svg.tsx +138 -138
- package/src/controls/toolbar.tsx +46 -46
- package/src/controls/vertical-content.tsx +49 -49
- package/src/controls/vertical.tsx +42 -42
- package/src/helpers/block-nav.tsx +88 -88
- package/src/helpers/context-const.ts +29 -29
- package/src/helpers/context-export.tsx +77 -77
- package/src/helpers/context-internal.ts +13 -13
- package/src/helpers/drag-drop/drag-drop-container.tsx +53 -53
- package/src/helpers/drag-drop/drag-drop-context-internal.tsx +9 -9
- package/src/helpers/drag-drop/drag-drop-context.tsx +61 -61
- package/src/helpers/drag-drop/drag-drop.types.ts +21 -21
- package/src/helpers/drag-drop/index.ts +12 -12
- package/src/helpers/drag-drop/readme.md +75 -75
- package/src/helpers/drag-drop/use-draggable.ts +47 -47
- package/src/helpers/drag-drop/use-droppable.ts +38 -38
- package/src/helpers/forwardRef.ts +7 -7
- package/src/helpers/hooks-events.ts +149 -149
- package/src/helpers/hooks.tsx +162 -141
- package/src/helpers/index.ts +8 -8
- package/src/helpers/use-alerts.tsx +74 -74
- package/src/helpers/use-editable-control.tsx +37 -37
- package/src/helpers/use-toast.tsx +29 -29
- package/src/index.ts +2 -2
- package/src/styles/index.ts +1 -1
- package/src/styles/styles.ts +104 -104
- package/src/styles/theme.ts +90 -90
package/src/controls/list.tsx
CHANGED
@@ -1,120 +1,120 @@
|
|
1
|
-
import { makeStyles, tokens } from '@fluentui/react-components';
|
2
|
-
import { LOGO_BLUE_SQUARE, LOGO_WHITE_SQUARE, isNullOrUndefined, isString } from '@kwiz/common';
|
3
|
-
import React from 'react';
|
4
|
-
import { useKWIZFluentContext } from '../helpers/context-internal';
|
5
|
-
import { KnownClassNames, mixins } from '../styles/styles';
|
6
|
-
import { Horizontal } from './horizontal';
|
7
|
-
import { Section } from './section';
|
8
|
-
import { Vertical } from './vertical';
|
9
|
-
|
10
|
-
const useStyles = makeStyles({
|
11
|
-
list: {
|
12
|
-
rowGap: 0
|
13
|
-
},
|
14
|
-
listItem: {
|
15
|
-
padding: tokens.spacingVerticalS,
|
16
|
-
':hover': {
|
17
|
-
backgroundColor: tokens.colorNeutralBackground1Hover
|
18
|
-
}
|
19
|
-
},
|
20
|
-
listItemSelected: {
|
21
|
-
backgroundColor: tokens.colorNeutralBackground1Selected
|
22
|
-
},
|
23
|
-
media: {
|
24
|
-
width: '32px',
|
25
|
-
fontSize: tokens.fontSizeBase600,
|
26
|
-
display: 'flex',
|
27
|
-
flexDirection: 'column',
|
28
|
-
justifyContent: 'center'
|
29
|
-
},
|
30
|
-
image: {
|
31
|
-
width: tokens.lineHeightBase600,
|
32
|
-
height: tokens.lineHeightBase600,
|
33
|
-
backgroundPosition: 'center center',
|
34
|
-
backgroundSize: 'cover',
|
35
|
-
borderRadius: tokens.borderRadiusCircular,
|
36
|
-
border: `1px solid ${tokens.colorNeutralStroke1}`
|
37
|
-
},
|
38
|
-
listItemBody: {
|
39
|
-
rowGap: 0,
|
40
|
-
width: 'calc(100% - 44px)'
|
41
|
-
},
|
42
|
-
listItemHeader: mixins.ellipsis,
|
43
|
-
listItemContent: {
|
44
|
-
...mixins.ellipsis,
|
45
|
-
fontSize: tokens.fontSizeBase200
|
46
|
-
},
|
47
|
-
listItemMedia: {
|
48
|
-
...mixins.ellipsis,
|
49
|
-
maxWidth: '20%',
|
50
|
-
'& svg': {
|
51
|
-
height: tokens.fontSizeBase300
|
52
|
-
},
|
53
|
-
'& button': {
|
54
|
-
padding: 0,
|
55
|
-
minWidth: 0,
|
56
|
-
minHeight: 0,
|
57
|
-
height: '14px'
|
58
|
-
}
|
59
|
-
},
|
60
|
-
listItemMediaNoTrim: {
|
61
|
-
overflow: 'visible',
|
62
|
-
maxWidth: 'fit-content'
|
63
|
-
},
|
64
|
-
listItemMultilineContent: {
|
65
|
-
whiteSpace: 'pre-line'
|
66
|
-
}
|
67
|
-
});
|
68
|
-
|
69
|
-
export interface iListItem {
|
70
|
-
key: string | number;
|
71
|
-
media?: JSX.Element | string;
|
72
|
-
header: string;
|
73
|
-
headerMedia?: JSX.Element | string;
|
74
|
-
content?: string | JSX.Element | (string | JSX.Element)[];
|
75
|
-
onClickOnMedia?: boolean;
|
76
|
-
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
77
|
-
selected?: boolean;
|
78
|
-
}
|
79
|
-
interface IProps {
|
80
|
-
selectable?: boolean;
|
81
|
-
items: iListItem[];
|
82
|
-
showAllHeaderMedia?: boolean;
|
83
|
-
/** allow multiline content */
|
84
|
-
multiline?: boolean;
|
85
|
-
dark?: boolean;
|
86
|
-
}
|
87
|
-
|
88
|
-
export const ListEx = (props: IProps) => {
|
89
|
-
const ctx = useKWIZFluentContext();
|
90
|
-
const cssNames = useStyles();
|
91
|
-
const isDark = ctx.dark === true || props.dark === true;
|
92
|
-
|
93
|
-
const listItemElm = (item: iListItem) => <Horizontal key={item.key} css={[cssNames.listItem, item.selected && cssNames.listItemSelected]} onClick={item.onClick}>
|
94
|
-
{item.media && <Section css={[cssNames.media]} onClick={(e) => {
|
95
|
-
if (!item.onClickOnMedia)
|
96
|
-
e.stopPropagation();//media may have its on onclick
|
97
|
-
}}>{
|
98
|
-
isString(item.media)
|
99
|
-
? <div className={cssNames.image} style={{ backgroundImage: `url('${encodeURI(item.media)}'), url('${isDark ? LOGO_WHITE_SQUARE : LOGO_BLUE_SQUARE}')` }}></div>
|
100
|
-
: item.media
|
101
|
-
}</Section>}
|
102
|
-
<Vertical main css={[cssNames.listItemBody]}>
|
103
|
-
<Horizontal main>
|
104
|
-
<Section main css={[cssNames.listItemHeader]}>{item.header}</Section>
|
105
|
-
{item.headerMedia && <Section onClick={(e) => {
|
106
|
-
e.stopPropagation();//media may have its on onclick
|
107
|
-
}} css={[cssNames.listItemMedia, props.showAllHeaderMedia && cssNames.listItemMediaNoTrim]}>{item.headerMedia}</Section>}
|
108
|
-
</Horizontal>
|
109
|
-
{!isNullOrUndefined(item.content)
|
110
|
-
? (Array.isArray(item.content) ? item.content : [item.content]).map((c, idx) => isNullOrUndefined(c) ? undefined : <Section key={idx} css={[cssNames.listItemContent, props.multiline ? cssNames.listItemMultilineContent : undefined]}>{c}</Section>)
|
111
|
-
: undefined}
|
112
|
-
</Vertical>
|
113
|
-
</Horizontal>;
|
114
|
-
|
115
|
-
return (
|
116
|
-
<Vertical css={[cssNames.list, KnownClassNames.list]}>
|
117
|
-
{props.items.map(item => listItemElm(item))}
|
118
|
-
</Vertical>
|
119
|
-
);
|
1
|
+
import { makeStyles, tokens } from '@fluentui/react-components';
|
2
|
+
import { LOGO_BLUE_SQUARE, LOGO_WHITE_SQUARE, isNullOrUndefined, isString } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { useKWIZFluentContext } from '../helpers/context-internal';
|
5
|
+
import { KnownClassNames, mixins } from '../styles/styles';
|
6
|
+
import { Horizontal } from './horizontal';
|
7
|
+
import { Section } from './section';
|
8
|
+
import { Vertical } from './vertical';
|
9
|
+
|
10
|
+
const useStyles = makeStyles({
|
11
|
+
list: {
|
12
|
+
rowGap: 0
|
13
|
+
},
|
14
|
+
listItem: {
|
15
|
+
padding: tokens.spacingVerticalS,
|
16
|
+
':hover': {
|
17
|
+
backgroundColor: tokens.colorNeutralBackground1Hover
|
18
|
+
}
|
19
|
+
},
|
20
|
+
listItemSelected: {
|
21
|
+
backgroundColor: tokens.colorNeutralBackground1Selected
|
22
|
+
},
|
23
|
+
media: {
|
24
|
+
width: '32px',
|
25
|
+
fontSize: tokens.fontSizeBase600,
|
26
|
+
display: 'flex',
|
27
|
+
flexDirection: 'column',
|
28
|
+
justifyContent: 'center'
|
29
|
+
},
|
30
|
+
image: {
|
31
|
+
width: tokens.lineHeightBase600,
|
32
|
+
height: tokens.lineHeightBase600,
|
33
|
+
backgroundPosition: 'center center',
|
34
|
+
backgroundSize: 'cover',
|
35
|
+
borderRadius: tokens.borderRadiusCircular,
|
36
|
+
border: `1px solid ${tokens.colorNeutralStroke1}`
|
37
|
+
},
|
38
|
+
listItemBody: {
|
39
|
+
rowGap: 0,
|
40
|
+
width: 'calc(100% - 44px)'
|
41
|
+
},
|
42
|
+
listItemHeader: mixins.ellipsis,
|
43
|
+
listItemContent: {
|
44
|
+
...mixins.ellipsis,
|
45
|
+
fontSize: tokens.fontSizeBase200
|
46
|
+
},
|
47
|
+
listItemMedia: {
|
48
|
+
...mixins.ellipsis,
|
49
|
+
maxWidth: '20%',
|
50
|
+
'& svg': {
|
51
|
+
height: tokens.fontSizeBase300
|
52
|
+
},
|
53
|
+
'& button': {
|
54
|
+
padding: 0,
|
55
|
+
minWidth: 0,
|
56
|
+
minHeight: 0,
|
57
|
+
height: '14px'
|
58
|
+
}
|
59
|
+
},
|
60
|
+
listItemMediaNoTrim: {
|
61
|
+
overflow: 'visible',
|
62
|
+
maxWidth: 'fit-content'
|
63
|
+
},
|
64
|
+
listItemMultilineContent: {
|
65
|
+
whiteSpace: 'pre-line'
|
66
|
+
}
|
67
|
+
});
|
68
|
+
|
69
|
+
export interface iListItem {
|
70
|
+
key: string | number;
|
71
|
+
media?: JSX.Element | string;
|
72
|
+
header: string;
|
73
|
+
headerMedia?: JSX.Element | string;
|
74
|
+
content?: string | JSX.Element | (string | JSX.Element)[];
|
75
|
+
onClickOnMedia?: boolean;
|
76
|
+
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
77
|
+
selected?: boolean;
|
78
|
+
}
|
79
|
+
interface IProps {
|
80
|
+
selectable?: boolean;
|
81
|
+
items: iListItem[];
|
82
|
+
showAllHeaderMedia?: boolean;
|
83
|
+
/** allow multiline content */
|
84
|
+
multiline?: boolean;
|
85
|
+
dark?: boolean;
|
86
|
+
}
|
87
|
+
|
88
|
+
export const ListEx = (props: IProps) => {
|
89
|
+
const ctx = useKWIZFluentContext();
|
90
|
+
const cssNames = useStyles();
|
91
|
+
const isDark = ctx.dark === true || props.dark === true;
|
92
|
+
|
93
|
+
const listItemElm = (item: iListItem) => <Horizontal key={item.key} css={[cssNames.listItem, item.selected && cssNames.listItemSelected]} onClick={item.onClick}>
|
94
|
+
{item.media && <Section css={[cssNames.media]} onClick={(e) => {
|
95
|
+
if (!item.onClickOnMedia)
|
96
|
+
e.stopPropagation();//media may have its on onclick
|
97
|
+
}}>{
|
98
|
+
isString(item.media)
|
99
|
+
? <div className={cssNames.image} style={{ backgroundImage: `url('${encodeURI(item.media)}'), url('${isDark ? LOGO_WHITE_SQUARE : LOGO_BLUE_SQUARE}')` }}></div>
|
100
|
+
: item.media
|
101
|
+
}</Section>}
|
102
|
+
<Vertical main css={[cssNames.listItemBody]}>
|
103
|
+
<Horizontal main>
|
104
|
+
<Section main css={[cssNames.listItemHeader]}>{item.header}</Section>
|
105
|
+
{item.headerMedia && <Section onClick={(e) => {
|
106
|
+
e.stopPropagation();//media may have its on onclick
|
107
|
+
}} css={[cssNames.listItemMedia, props.showAllHeaderMedia && cssNames.listItemMediaNoTrim]}>{item.headerMedia}</Section>}
|
108
|
+
</Horizontal>
|
109
|
+
{!isNullOrUndefined(item.content)
|
110
|
+
? (Array.isArray(item.content) ? item.content : [item.content]).map((c, idx) => isNullOrUndefined(c) ? undefined : <Section key={idx} css={[cssNames.listItemContent, props.multiline ? cssNames.listItemMultilineContent : undefined]}>{c}</Section>)
|
111
|
+
: undefined}
|
112
|
+
</Vertical>
|
113
|
+
</Horizontal>;
|
114
|
+
|
115
|
+
return (
|
116
|
+
<Vertical css={[cssNames.list, KnownClassNames.list]}>
|
117
|
+
{props.items.map(item => listItemElm(item))}
|
118
|
+
</Vertical>
|
119
|
+
);
|
120
120
|
}
|
package/src/controls/loading.tsx
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
import { LOGO_ANIM_SMALL } from '@kwiz/common';
|
2
|
-
import { Centered } from './centered';
|
3
|
-
import React from 'react';
|
4
|
-
|
5
|
-
interface IProps {
|
6
|
-
}
|
7
|
-
export const Loading: React.FunctionComponent<IProps> = (props) => {
|
8
|
-
return (
|
9
|
-
<Centered><img src={LOGO_ANIM_SMALL} alt="loading" style={{ width: '15vw' }} /></Centered>
|
10
|
-
);
|
1
|
+
import { LOGO_ANIM_SMALL } from '@kwiz/common';
|
2
|
+
import { Centered } from './centered';
|
3
|
+
import React from 'react';
|
4
|
+
|
5
|
+
interface IProps {
|
6
|
+
}
|
7
|
+
export const Loading: React.FunctionComponent<IProps> = (props) => {
|
8
|
+
return (
|
9
|
+
<Centered><img src={LOGO_ANIM_SMALL} alt="loading" style={{ width: '15vw' }} /></Centered>
|
10
|
+
);
|
11
11
|
}
|
package/src/controls/menu.tsx
CHANGED
@@ -1,174 +1,196 @@
|
|
1
|
-
import { Menu, MenuDivider, MenuGroup, MenuGroupHeader, MenuItem, MenuList, MenuListProps, MenuPopover, menuPopoverClassNames, MenuPopoverProps, MenuProps, MenuTrigger } from '@fluentui/react-components';
|
2
|
-
import {
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import { useKWIZFluentContext } from '../helpers/context-internal';
|
6
|
-
import {
|
7
|
-
import {
|
8
|
-
import { Horizontal } from './horizontal';
|
9
|
-
import { Search } from './search';
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
items: iMenuItemEX[];
|
28
|
-
}
|
29
|
-
export type iMenuItemEX = iMenuItemEXItem | iMenuItemEXSeparator | iMenuItemEXGroup;
|
30
|
-
|
31
|
-
interface IProps {
|
32
|
-
menuProps?: MenuProps;
|
33
|
-
menuPopOverProps?: MenuPopoverProps;
|
34
|
-
menuListProps?: MenuListProps;
|
35
|
-
trigger: JSX.Element | string | ButtonEXProps;
|
36
|
-
items: iMenuItemEX[];
|
37
|
-
/** default 8 null/false to disable */
|
38
|
-
filterThreshold?: number | false;
|
39
|
-
/** default 8, null/false to disable */
|
40
|
-
pageSize?: number | false;
|
41
|
-
}
|
42
|
-
export const MenuEx: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
|
43
|
-
const ctx = useKWIZFluentContext();
|
44
|
-
const [startIndexPerLevel, setStartIndexPerLevel] = useStateEX<IDictionary<number>>({});
|
45
|
-
const [filterPerLevel, setFilterPerLevel] = useStateEX<IDictionary<string>>({});
|
46
|
-
let pageSize: number = isUndefined(props.pageSize) ? 8 : isNumber(props.pageSize) ? props.pageSize : 99999999999;
|
47
|
-
let filterThreshold: number = isUndefined(props.filterThreshold) ? 8 : isNumber(props.filterThreshold) ? props.filterThreshold : 99999999999;
|
48
|
-
|
49
|
-
//when hovering over sub menu the parent would close - have menu trigger keep open on the parent level
|
50
|
-
const [keepOpen, setKeepOpen] = useStateEX<IDictionary<boolean>>({});
|
51
|
-
const [opened, setOpened] = useStateEX<IDictionary<boolean>>({});
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
1
|
+
import { Menu, MenuDivider, MenuGroup, MenuGroupHeader, MenuItem, MenuList, MenuListProps, MenuPopover, menuPopoverClassNames, MenuPopoverProps, MenuProps, MenuTrigger } from '@fluentui/react-components';
|
2
|
+
import { IDictionary, isNotEmptyArray, isNotEmptyString, isNullOrEmptyString, isNullOrUndefined, isNumber, isString, isUndefined, jsonClone, stopEvent } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { useClickableDiv, useStateEX } from '../helpers';
|
5
|
+
import { useKWIZFluentContext } from '../helpers/context-internal';
|
6
|
+
import { ButtonEX, ButtonEXProps } from './button';
|
7
|
+
import { DividerEX } from './divider';
|
8
|
+
import { Horizontal } from './horizontal';
|
9
|
+
import { Search } from './search';
|
10
|
+
|
11
|
+
interface iMenuItemEXItem {
|
12
|
+
type?: "item";
|
13
|
+
title: string;
|
14
|
+
onClick: () => void;
|
15
|
+
disabled?: boolean;
|
16
|
+
icon?: JSX.Element;
|
17
|
+
items?: iMenuItemEX[];
|
18
|
+
checked?: boolean;
|
19
|
+
}
|
20
|
+
interface iMenuItemEXSeparator {
|
21
|
+
type: "separator";
|
22
|
+
}
|
23
|
+
interface iMenuItemEXGroup {
|
24
|
+
type: "group";
|
25
|
+
title: string;
|
26
|
+
//can't nest groups
|
27
|
+
items: (iMenuItemEX & { type?: "separator" | "item" })[];
|
28
|
+
}
|
29
|
+
export type iMenuItemEX = iMenuItemEXItem | iMenuItemEXSeparator | iMenuItemEXGroup;
|
30
|
+
|
31
|
+
interface IProps {
|
32
|
+
menuProps?: MenuProps;
|
33
|
+
menuPopOverProps?: MenuPopoverProps;
|
34
|
+
menuListProps?: MenuListProps;
|
35
|
+
trigger: JSX.Element | string | ButtonEXProps;
|
36
|
+
items: iMenuItemEX[];
|
37
|
+
/** default 8 null/false to disable */
|
38
|
+
filterThreshold?: number | false;
|
39
|
+
/** default 8, null/false to disable */
|
40
|
+
pageSize?: number | false;
|
41
|
+
}
|
42
|
+
export const MenuEx: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
|
43
|
+
const ctx = useKWIZFluentContext();
|
44
|
+
const [startIndexPerLevel, setStartIndexPerLevel] = useStateEX<IDictionary<number>>({});
|
45
|
+
const [filterPerLevel, setFilterPerLevel] = useStateEX<IDictionary<string>>({});
|
46
|
+
let pageSize: number = isUndefined(props.pageSize) ? 8 : isNumber(props.pageSize) ? props.pageSize : 99999999999;
|
47
|
+
let filterThreshold: number = isUndefined(props.filterThreshold) ? 8 : isNumber(props.filterThreshold) ? props.filterThreshold : 99999999999;
|
48
|
+
|
49
|
+
//when hovering over sub menu the parent would close - have menu trigger keep open on the parent level
|
50
|
+
const [keepOpen, setKeepOpen] = useStateEX<IDictionary<boolean>>({});
|
51
|
+
const [opened, setOpened] = useStateEX<IDictionary<boolean>>({});
|
52
|
+
|
53
|
+
const clickableDiv = useClickableDiv();
|
54
|
+
|
55
|
+
React.useEffect(() => {
|
56
|
+
window.setTimeout(() => {
|
57
|
+
var menus = document.querySelectorAll(`.${menuPopoverClassNames.root}`);
|
58
|
+
menus.forEach((menu: HTMLDivElement) => {
|
59
|
+
var rect = menu.getBoundingClientRect();
|
60
|
+
if (rect.bottom > document.documentElement.clientHeight) {
|
61
|
+
menu.style.overflow = "auto";
|
62
|
+
menu.style.height = `${rect.height - (rect.bottom - document.documentElement.clientHeight)}px`;
|
63
|
+
}
|
64
|
+
});
|
65
|
+
}, 100);
|
66
|
+
}, [opened]);
|
67
|
+
|
68
|
+
function renderItems(items: iMenuItemEX[], level: number) {
|
69
|
+
const myLevelFilter = filterPerLevel[level];
|
70
|
+
|
71
|
+
const showItem = (i: iMenuItemEX) => {
|
72
|
+
//get rid of empty/null items
|
73
|
+
let show = !isNullOrUndefined(i) && (isNotEmptyString(i.type) || isNotEmptyString((i as iMenuItemEXItem).title));
|
74
|
+
if (show && isNotEmptyString(myLevelFilter)) {
|
75
|
+
if (i.type === "separator") show = false;
|
76
|
+
else if (i.type === "group") {
|
77
|
+
//only show group if 1 or more results are in it
|
78
|
+
return i.items.filter(sub => showItem(sub)).length > 0;
|
79
|
+
}
|
80
|
+
else
|
81
|
+
show = i.title.toLowerCase().indexOf(myLevelFilter) >= 0;
|
82
|
+
}
|
83
|
+
return show;
|
84
|
+
}
|
85
|
+
|
86
|
+
//inject group items into this level - so we share the filter/next functionality. it looks wierd if filter/paging is done per group if they are displayed inline.
|
87
|
+
items = items.map(i => i.type === "group" && isNotEmptyArray(i.items) ? [i, ...i.items] : i)
|
88
|
+
.flat()
|
89
|
+
//filter empty item or based on text filter
|
90
|
+
.filter(i => showItem(i));
|
91
|
+
|
92
|
+
let menuItems = items.map((item, index) => {
|
93
|
+
switch (item.type) {
|
94
|
+
case "group":
|
95
|
+
//todo: technically group items should be nested inside the group for better screen reder support
|
96
|
+
return <MenuGroup key={index}>
|
97
|
+
<MenuGroupHeader>{item.title}</MenuGroupHeader>
|
98
|
+
</MenuGroup>;
|
99
|
+
case "separator":
|
100
|
+
return <MenuDivider key={index} />;
|
101
|
+
case "item":
|
102
|
+
default:
|
103
|
+
const openKey = `${level}|${index}`;
|
104
|
+
const menuItem = <MenuItem key={index} icon={item.icon}
|
105
|
+
disabled={item.disabled}
|
106
|
+
onClick={item.onClick}
|
107
|
+
>{item.title}</MenuItem>;
|
108
|
+
return isNotEmptyArray(item.items)
|
109
|
+
? <Menu key={index} mountNode={ctx.mountNode} open={opened[openKey] || false} onOpenChange={(e, data) => {
|
110
|
+
if (data.open) {
|
111
|
+
setOpened({ ...opened, [openKey]: true });
|
112
|
+
setKeepOpen({ ...keepOpen, [level]: true });
|
113
|
+
}
|
114
|
+
else if (!keepOpen[openKey]) {
|
115
|
+
setOpened({ ...opened, [openKey]: false });
|
116
|
+
setKeepOpen({ ...keepOpen, [level]: false });
|
117
|
+
}
|
118
|
+
}}>
|
119
|
+
<MenuTrigger disableButtonEnhancement>
|
120
|
+
{menuItem}
|
121
|
+
</MenuTrigger>
|
122
|
+
<MenuPopover>
|
123
|
+
<MenuList>
|
124
|
+
{renderItems(item.items, level + 1)}
|
125
|
+
</MenuList>
|
126
|
+
</MenuPopover>
|
127
|
+
</Menu>
|
128
|
+
: menuItem;
|
129
|
+
}
|
130
|
+
});
|
131
|
+
|
132
|
+
const paged = menuItems.length > pageSize;
|
133
|
+
const filtered = menuItems.length > filterThreshold || !isNullOrEmptyString(myLevelFilter);
|
134
|
+
|
135
|
+
if (paged) {
|
136
|
+
let start = startIndexPerLevel[level];
|
137
|
+
if (isNullOrUndefined(start)) start = 0;
|
138
|
+
let hasMore = menuItems.length > start + pageSize;
|
139
|
+
menuItems = menuItems.slice(start, start + pageSize);
|
140
|
+
if (start > 0) {
|
141
|
+
menuItems.splice(0, 0, <DividerEX key="$prev" title='Previous'
|
142
|
+
{...clickableDiv}
|
143
|
+
onClick={() => {
|
144
|
+
const s = jsonClone(startIndexPerLevel);
|
145
|
+
s[level] = start - pageSize;
|
146
|
+
setStartIndexPerLevel(s);
|
147
|
+
}}
|
148
|
+
>previous</DividerEX>);
|
149
|
+
}
|
150
|
+
if (hasMore)
|
151
|
+
menuItems.push(<DividerEX key="$next" title='Next'
|
152
|
+
{...clickableDiv}
|
153
|
+
onClick={() => {
|
154
|
+
const s = jsonClone(startIndexPerLevel);
|
155
|
+
s[level] = start + pageSize;
|
156
|
+
setStartIndexPerLevel(s);
|
157
|
+
}}
|
158
|
+
>next</DividerEX>);
|
159
|
+
}
|
160
|
+
if (filtered) {
|
161
|
+
//just filter - no paging
|
162
|
+
menuItems.splice(0, 0, <Horizontal key='$search'>
|
163
|
+
<Search defaultValue={myLevelFilter || ""} onChangeDeferred={(newValue) => {
|
164
|
+
const s = jsonClone(filterPerLevel);
|
165
|
+
s[level] = newValue ? newValue.toLowerCase() : "";
|
166
|
+
setFilterPerLevel(s);
|
167
|
+
}} />
|
168
|
+
</Horizontal>);
|
169
|
+
}
|
170
|
+
return menuItems;
|
171
|
+
}
|
172
|
+
|
173
|
+
return (
|
174
|
+
<Menu mountNode={ctx.mountNode} {...props.menuProps} open={opened[0] || false} onOpenChange={(e, data) => {
|
175
|
+
if (data.open) setOpened({ ...opened, 0: true });
|
176
|
+
else if (!keepOpen[0]) setOpened({ ...opened, 0: false });
|
177
|
+
}}>
|
178
|
+
<MenuTrigger disableButtonEnhancement>
|
179
|
+
{isString(props.trigger)
|
180
|
+
? <ButtonEX title={props.trigger} onClick={(e) => {
|
181
|
+
stopEvent(e);
|
182
|
+
}} />
|
183
|
+
: isString((props.trigger as ButtonEXProps).title)
|
184
|
+
? <ButtonEX {...(props.trigger as ButtonEXProps)} onClick={(e) => {
|
185
|
+
stopEvent(e);
|
186
|
+
}} />
|
187
|
+
: props.trigger as JSX.Element}
|
188
|
+
</MenuTrigger>
|
189
|
+
<MenuPopover {...props.menuPopOverProps}>
|
190
|
+
<MenuList {...props.menuListProps}>
|
191
|
+
{renderItems(props.items, 0)}
|
192
|
+
</MenuList>
|
193
|
+
</MenuPopover>
|
194
|
+
</Menu>
|
195
|
+
);
|
174
196
|
}
|