@kwiz/fluentui 1.0.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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/_modules/build.d.ts +2 -0
- package/dist/_modules/build.js +3 -0
- package/dist/_modules/build.js.map +1 -0
- package/dist/_modules/config.d.ts +1 -0
- package/dist/_modules/config.js +9 -0
- package/dist/_modules/config.js.map +1 -0
- package/dist/_modules/constants.d.ts +2 -0
- package/dist/_modules/constants.js +3 -0
- package/dist/_modules/constants.js.map +1 -0
- package/dist/_modules/exports-index.d.ts +1 -0
- package/dist/_modules/exports-index.js +2 -0
- package/dist/_modules/exports-index.js.map +1 -0
- package/dist/controls/button.d.ts +27 -0
- package/dist/controls/button.js +100 -0
- package/dist/controls/button.js.map +1 -0
- package/dist/controls/centered.d.ts +5 -0
- package/dist/controls/centered.js +14 -0
- package/dist/controls/centered.js.map +1 -0
- package/dist/controls/dropdown.d.ts +18 -0
- package/dist/controls/dropdown.js +13 -0
- package/dist/controls/dropdown.js.map +1 -0
- package/dist/controls/error-boundary.d.ts +23 -0
- package/dist/controls/error-boundary.js +33 -0
- package/dist/controls/error-boundary.js.map +1 -0
- package/dist/controls/exports-index.d.ts +17 -0
- package/dist/controls/exports-index.js +18 -0
- package/dist/controls/exports-index.js.map +1 -0
- package/dist/controls/field-editor.d.ts +13 -0
- package/dist/controls/field-editor.js +15 -0
- package/dist/controls/field-editor.js.map +1 -0
- package/dist/controls/file-upload.d.ts +18 -0
- package/dist/controls/file-upload.js +41 -0
- package/dist/controls/file-upload.js.map +1 -0
- package/dist/controls/horizontal.d.ts +8 -0
- package/dist/controls/horizontal.js +23 -0
- package/dist/controls/horizontal.js.map +1 -0
- package/dist/controls/input.d.ts +13 -0
- package/dist/controls/input.js +41 -0
- package/dist/controls/input.js.map +1 -0
- package/dist/controls/list.d.ts +21 -0
- package/dist/controls/list.js +72 -0
- package/dist/controls/list.js.map +1 -0
- package/dist/controls/loading.d.ts +5 -0
- package/dist/controls/loading.js +7 -0
- package/dist/controls/loading.js.map +1 -0
- package/dist/controls/please-wait.d.ts +17 -0
- package/dist/controls/please-wait.js +16 -0
- package/dist/controls/please-wait.js.map +1 -0
- package/dist/controls/prompt.d.ts +16 -0
- package/dist/controls/prompt.js +21 -0
- package/dist/controls/prompt.js.map +1 -0
- package/dist/controls/search.d.ts +13 -0
- package/dist/controls/search.js +47 -0
- package/dist/controls/search.js.map +1 -0
- package/dist/controls/section.d.ts +14 -0
- package/dist/controls/section.js +27 -0
- package/dist/controls/section.js.map +1 -0
- package/dist/controls/svg.d.ts +23 -0
- package/dist/controls/svg.js +45 -0
- package/dist/controls/svg.js.map +1 -0
- package/dist/controls/vertical-content.d.ts +6 -0
- package/dist/controls/vertical-content.js +37 -0
- package/dist/controls/vertical-content.js.map +1 -0
- package/dist/controls/vertical.d.ts +8 -0
- package/dist/controls/vertical.js +23 -0
- package/dist/controls/vertical.js.map +1 -0
- package/dist/exports-index.d.ts +3 -0
- package/dist/exports-index.js +4 -0
- package/dist/exports-index.js.map +1 -0
- package/dist/helpers/hooks.d.ts +22 -0
- package/dist/helpers/hooks.js +173 -0
- package/dist/helpers/hooks.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/styles/exports-index.d.ts +2 -0
- package/dist/styles/exports-index.js +3 -0
- package/dist/styles/exports-index.js.map +1 -0
- package/dist/styles/styles.d.ts +19 -0
- package/dist/styles/styles.js +79 -0
- package/dist/styles/styles.js.map +1 -0
- package/dist/styles/theme.d.ts +6 -0
- package/dist/styles/theme.js +77 -0
- package/dist/styles/theme.js.map +1 -0
- package/package.json +71 -0
- package/src/_modules/config.ts +9 -0
- package/src/_modules/constants.ts +3 -0
- package/src/controls/button.tsx +154 -0
- package/src/controls/centered.tsx +23 -0
- package/src/controls/dropdown.tsx +28 -0
- package/src/controls/error-boundary.tsx +42 -0
- package/src/controls/field-editor.tsx +42 -0
- package/src/controls/file-upload.tsx +68 -0
- package/src/controls/horizontal.tsx +35 -0
- package/src/controls/input.tsx +59 -0
- package/src/controls/list.tsx +118 -0
- package/src/controls/loading.tsx +11 -0
- package/src/controls/please-wait.tsx +32 -0
- package/src/controls/prompt.tsx +59 -0
- package/src/controls/search.tsx +66 -0
- package/src/controls/section.tsx +52 -0
- package/src/controls/svg.tsx +120 -0
- package/src/controls/vertical-content.tsx +50 -0
- package/src/controls/vertical.tsx +35 -0
- package/src/helpers/hooks.ts +189 -0
- package/src/index.ts +19 -0
- package/src/styles/styles.ts +87 -0
- package/src/styles/theme.ts +91 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
import { Button, ButtonProps, CompoundButton, compoundButtonClassNames, CompoundButtonProps, makeStyles, mergeClasses, tokens, Tooltip } from '@fluentui/react-components';
|
2
|
+
import { capitalizeFirstLetter, isFunction, isNullOrEmptyString, isNullOrUndefined, isString, PushNoDuplicate } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { useCommonStyles, widthMedium } from '../styles/styles';
|
5
|
+
|
6
|
+
interface IProps {
|
7
|
+
title: string;//required
|
8
|
+
showTitleWithIcon?: boolean;
|
9
|
+
dontStretch?: boolean;
|
10
|
+
hideOnPrint?: boolean;
|
11
|
+
dontCenterText?: boolean;
|
12
|
+
hoverIcon?: JSX.Element;
|
13
|
+
hoverTitle?: string;
|
14
|
+
}
|
15
|
+
interface IPropsCompound extends IProps {
|
16
|
+
width?: string | number;
|
17
|
+
}
|
18
|
+
|
19
|
+
export type ButtonEXProps = IProps & ButtonProps;
|
20
|
+
export type CompoundButtonEXProps = IPropsCompound & CompoundButtonProps;
|
21
|
+
|
22
|
+
const useStyles = makeStyles({
|
23
|
+
buttonNoCenter: {
|
24
|
+
justifyContent: 'flex-start',
|
25
|
+
'& *': {
|
26
|
+
/* a button with no center that has content of a vertical, or multiple labels */
|
27
|
+
alignItems: 'flex-start'
|
28
|
+
}
|
29
|
+
},
|
30
|
+
danger: {
|
31
|
+
backgroundColor: tokens.colorStatusDangerBackground1,
|
32
|
+
color: tokens.colorStatusWarningForeground2,
|
33
|
+
|
34
|
+
[`& .${compoundButtonClassNames.secondaryContent}`]: {
|
35
|
+
color: tokens.colorStatusWarningForeground1
|
36
|
+
}
|
37
|
+
},
|
38
|
+
success: {
|
39
|
+
color: tokens.colorStatusSuccessForeground1,
|
40
|
+
|
41
|
+
[`& .${compoundButtonClassNames.secondaryContent}`]: {
|
42
|
+
color: tokens.colorStatusSuccessForeground1,
|
43
|
+
}
|
44
|
+
},
|
45
|
+
primarySubtle: {
|
46
|
+
color: tokens.colorBrandForeground1,
|
47
|
+
|
48
|
+
[`& .${compoundButtonClassNames.secondaryContent}`]: {
|
49
|
+
color: tokens.colorBrandForeground1,
|
50
|
+
}
|
51
|
+
},
|
52
|
+
})
|
53
|
+
|
54
|
+
|
55
|
+
export const ButtonEX = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
|
56
|
+
const [hover, setHover] = React.useState(false);
|
57
|
+
const trackHover = !isNullOrEmptyString(props.hoverTitle) || !isNullOrUndefined(props.hoverIcon);
|
58
|
+
|
59
|
+
const title = hover && !isNullOrEmptyString(props.hoverTitle) ? props.hoverTitle
|
60
|
+
: props.title || props['aria-label'];
|
61
|
+
const icon = hover && !isNullOrUndefined(props.hoverIcon) ? props.hoverIcon : props.icon;
|
62
|
+
let hasIcon = !isNullOrUndefined(icon);
|
63
|
+
let hasText = props.children || !hasIcon || (hasIcon && props.showTitleWithIcon === true);
|
64
|
+
|
65
|
+
const commonCssNames = useCommonStyles();
|
66
|
+
const cssNames = useStyles();
|
67
|
+
let css: string[] = [];
|
68
|
+
|
69
|
+
if (props.hideOnPrint) PushNoDuplicate(css, commonCssNames.printHide);
|
70
|
+
if (props.dontCenterText) PushNoDuplicate(css, cssNames.buttonNoCenter);
|
71
|
+
|
72
|
+
if (!isNullOrEmptyString(props.className)) css.push(...props.className.split(' '));
|
73
|
+
|
74
|
+
let btn = <Button ref={ref} appearance='subtle' {...props} className={mergeClasses(...css)}
|
75
|
+
aria-label={title} title={undefined} icon={icon}
|
76
|
+
onMouseEnter={trackHover ? (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
77
|
+
setHover(true);
|
78
|
+
if (isFunction(props.onMouseEnter))
|
79
|
+
props.onMouseEnter(e as any);
|
80
|
+
} : props.onMouseEnter as any}
|
81
|
+
onMouseLeave={trackHover ? (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
82
|
+
setHover(false);
|
83
|
+
if (isFunction(props.onMouseLeave))
|
84
|
+
props.onMouseLeave(e as any);
|
85
|
+
} : props.onMouseLeave as any}
|
86
|
+
>{props.children ||
|
87
|
+
//no icon? will show the title by default
|
88
|
+
(hasText && capitalizeFirstLetter(title))}</Button>;
|
89
|
+
if (!hasText || props.children)//icon only or when content is different than props.title
|
90
|
+
btn = <Tooltip showDelay={1000} relationship='label' withArrow appearance='inverted' content={title}>
|
91
|
+
{btn}
|
92
|
+
</Tooltip>;
|
93
|
+
|
94
|
+
return (
|
95
|
+
props.dontStretch ? <div>{btn}</div> : btn
|
96
|
+
|
97
|
+
);
|
98
|
+
});
|
99
|
+
export const ButtonEXSecondary = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
|
100
|
+
return (
|
101
|
+
<ButtonEX ref={ref} appearance='secondary' shape='circular' {...props}></ButtonEX>
|
102
|
+
);
|
103
|
+
});
|
104
|
+
export const ButtonEXPrimary = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
|
105
|
+
return (
|
106
|
+
<ButtonEXSecondary ref={ref} appearance='primary' {...props}>{props.children}</ButtonEXSecondary>
|
107
|
+
);
|
108
|
+
});
|
109
|
+
export const ButtonEXDanger = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
|
110
|
+
const cssNames = useStyles();
|
111
|
+
return (
|
112
|
+
<ButtonEXSecondary ref={ref} className={props.disabled ? undefined : cssNames.danger} {...props}>{props.children}</ButtonEXSecondary>
|
113
|
+
);
|
114
|
+
});
|
115
|
+
export const ButtonEXSuccess = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
|
116
|
+
const cssNames = useStyles();
|
117
|
+
return (
|
118
|
+
<ButtonEX ref={ref} className={cssNames.success} {...props}>{props.children}</ButtonEX>
|
119
|
+
);
|
120
|
+
});
|
121
|
+
export const ButtonEXPrimarySubtle = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
|
122
|
+
const cssNames = useStyles();
|
123
|
+
return (
|
124
|
+
<ButtonEX ref={ref} className={props.disabled ? undefined : cssNames.primarySubtle} {...props}>{props.children}</ButtonEX>
|
125
|
+
);
|
126
|
+
});
|
127
|
+
|
128
|
+
export const CompoundButtonEX = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
|
129
|
+
let title = props.title || props['aria-label'];
|
130
|
+
let tooltip = isString(props.secondaryContent) ? props.secondaryContent : title;
|
131
|
+
let max = typeof (props.width) === "undefined" ? widthMedium : props.width;
|
132
|
+
return (
|
133
|
+
<Tooltip showDelay={1000} relationship='label' withArrow appearance='inverted' content={tooltip}>
|
134
|
+
<CompoundButton ref={ref} appearance='subtle' style={{ justifyContent: "flex-start", maxWidth: max }} {...props} aria-label={tooltip} title={undefined}>
|
135
|
+
{props.children || capitalizeFirstLetter(title)}</CompoundButton>
|
136
|
+
</Tooltip>
|
137
|
+
);
|
138
|
+
});
|
139
|
+
export const CompoundButtonEXSecondary = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
|
140
|
+
return (
|
141
|
+
<CompoundButtonEX ref={ref} appearance='secondary' shape='circular' {...props}></CompoundButtonEX>
|
142
|
+
);
|
143
|
+
});
|
144
|
+
export const CompoundButtonEXPrimary = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
|
145
|
+
return (
|
146
|
+
<CompoundButtonEXSecondary ref={ref} appearance='primary' {...props}>{props.children}</CompoundButtonEXSecondary>
|
147
|
+
);
|
148
|
+
});
|
149
|
+
export const CompoundButtonEXDanger = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
|
150
|
+
const cssNames = useStyles();
|
151
|
+
return (
|
152
|
+
<CompoundButtonEXSecondary ref={ref} className={cssNames.danger} {...props}>{props.children}</CompoundButtonEXSecondary>
|
153
|
+
);
|
154
|
+
});
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { makeStyles } from '@fluentui/react-components';
|
2
|
+
import React from 'react';
|
3
|
+
import { Horizontal } from './horizontal';
|
4
|
+
import { Vertical } from './vertical';
|
5
|
+
|
6
|
+
const useStyles = makeStyles({
|
7
|
+
center: {
|
8
|
+
justifyContent: 'center'
|
9
|
+
},
|
10
|
+
})
|
11
|
+
|
12
|
+
interface IProps {
|
13
|
+
}
|
14
|
+
export const Centered: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
|
15
|
+
const cssNames = useStyles();
|
16
|
+
return (
|
17
|
+
<Vertical main css={[cssNames.center]}>
|
18
|
+
<Horizontal css={[cssNames.center]}>
|
19
|
+
{props.children}
|
20
|
+
</Horizontal>
|
21
|
+
</Vertical>
|
22
|
+
);
|
23
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Dropdown, Option } from '@fluentui/react-components';
|
2
|
+
import { filterEmptyEntries, firstOrNull } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
|
5
|
+
interface IProps<dataType, keyType extends string = string> {
|
6
|
+
selected: keyType[];
|
7
|
+
items: { key: keyType, value: string, data?: dataType, option?: JSX.Element; }[];
|
8
|
+
onSelect: (item: { key: keyType, value: string, data?: dataType }) => void;
|
9
|
+
placeholder?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
function $DropdownEX<keyType extends string = string, dataType = never>(props: IProps<dataType, keyType>, ref: React.ForwardedRef<HTMLButtonElement>) {
|
13
|
+
let text = filterEmptyEntries(props.selected.map(s => {
|
14
|
+
let v = firstOrNull(props.items, i => i.key === s);
|
15
|
+
return v ? v.value : ''
|
16
|
+
})).join(', ');
|
17
|
+
return (
|
18
|
+
<Dropdown placeholder={props.placeholder} ref={ref} appearance='underline' selectedOptions={props.selected} value={text} >
|
19
|
+
{props.items.map(i => <Option key={i.key} value={i.key}
|
20
|
+
onClick={() => { props.onSelect(i); }}
|
21
|
+
text={i.value}
|
22
|
+
>{i.option ? i.option : i.value}</Option>)}
|
23
|
+
</Dropdown>
|
24
|
+
|
25
|
+
);
|
26
|
+
}
|
27
|
+
|
28
|
+
export const DropdownEX = React.forwardRef($DropdownEX);
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import * as React from "react";
|
2
|
+
import { GetLogger } from "../_modules/config";
|
3
|
+
|
4
|
+
const logger = GetLogger("ErrorBoundary");
|
5
|
+
|
6
|
+
interface iProps {
|
7
|
+
errorComponent?: JSX.Element,
|
8
|
+
/** If changeMarker changes, it will check the error again */
|
9
|
+
changeMarker: string | number
|
10
|
+
}
|
11
|
+
interface iState { hasError: boolean; marker: string | number; }
|
12
|
+
export class ErrorBoundary extends React.Component<React.PropsWithChildren<iProps>, iState> {
|
13
|
+
constructor(props: iProps) {
|
14
|
+
super(props);
|
15
|
+
this.state = { hasError: false, marker: props.changeMarker };
|
16
|
+
}
|
17
|
+
|
18
|
+
static getDerivedStateFromError(error) {
|
19
|
+
// Update state so the next render will show the fallback UI.
|
20
|
+
return { hasError: true };
|
21
|
+
}
|
22
|
+
static getDerivedStateFromProps(props: iProps, state: iState) {
|
23
|
+
if (props.changeMarker !== state.marker)
|
24
|
+
return { hasError: false, marker: props.changeMarker };
|
25
|
+
else return null;
|
26
|
+
}
|
27
|
+
|
28
|
+
componentDidCatch(error, errorInfo) {
|
29
|
+
// You can also log the error to an error reporting service
|
30
|
+
logger.error(error);
|
31
|
+
logger.error(errorInfo);
|
32
|
+
}
|
33
|
+
|
34
|
+
render() {
|
35
|
+
if (this.state.hasError) {
|
36
|
+
// You can render any custom fallback UI
|
37
|
+
return this.props.errorComponent || <h1>Something went wrong.</h1>;
|
38
|
+
}
|
39
|
+
|
40
|
+
return this.props.children;
|
41
|
+
}
|
42
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import { Field, mergeClasses, Textarea } from '@fluentui/react-components';
|
2
|
+
import { isNullOrUndefined } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { GetLogger } from '../_modules/config';
|
5
|
+
import { InputEx } from './input';
|
6
|
+
|
7
|
+
const logger = GetLogger('FieldEditor');
|
8
|
+
|
9
|
+
interface IProps {
|
10
|
+
required?: boolean;
|
11
|
+
error?: string;
|
12
|
+
value: string;
|
13
|
+
onChange: (newValue: string) => void;
|
14
|
+
css: string[];
|
15
|
+
label: string;
|
16
|
+
description?: string;
|
17
|
+
type?: "text" | "multiline"
|
18
|
+
}
|
19
|
+
export const FieldEditor: React.FunctionComponent<IProps> = (props) => {
|
20
|
+
if (isNullOrUndefined(props.value)) {
|
21
|
+
logger.error(`${props.label}: value should not be null`);
|
22
|
+
}
|
23
|
+
return (
|
24
|
+
<Field required={props.required}
|
25
|
+
validationMessage={props.error || props.description}
|
26
|
+
validationState={props.error ? "error" : "none"}>
|
27
|
+
{props.type === "multiline"
|
28
|
+
? <Textarea className={props.css && mergeClasses(...props.css)}
|
29
|
+
required={props.required}
|
30
|
+
placeholder={props.label}
|
31
|
+
value={props.value || ""}
|
32
|
+
onChange={(e, data) => props.onChange(data.value)}
|
33
|
+
/>
|
34
|
+
: <InputEx className={props.css && mergeClasses(...props.css)}
|
35
|
+
required={props.required}
|
36
|
+
appearance='underline'
|
37
|
+
placeholder={props.label}
|
38
|
+
value={props.value || ""}
|
39
|
+
onChange={(e, data) => props.onChange(data.value)} />}
|
40
|
+
</Field>
|
41
|
+
);
|
42
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import { ButtonProps } from "@fluentui/react-components";
|
2
|
+
import { isFunction, isNotEmptyArray, isNullOrEmptyString } from '@kwiz/common';
|
3
|
+
import * as React from "react";
|
4
|
+
import { ButtonEX, CompoundButtonEXSecondary } from "./button";
|
5
|
+
|
6
|
+
interface iProps {
|
7
|
+
showTitleWithIcon?: boolean;
|
8
|
+
title?: string;
|
9
|
+
/** Passing this will turn the button into a compound button */
|
10
|
+
secondaryContent?: string;
|
11
|
+
limitFileTypes?: string[];
|
12
|
+
allowMultiple?: boolean;
|
13
|
+
icon?: JSX.Element;
|
14
|
+
onChange?: (newFile: File | FileList) => void;
|
15
|
+
/** only works for single file, reads it as base64 */
|
16
|
+
asBase64?: (base64: string) => void;
|
17
|
+
buttonProps?: ButtonProps;
|
18
|
+
disabled?: boolean;
|
19
|
+
}
|
20
|
+
|
21
|
+
export const FileUpload = React.forwardRef<HTMLButtonElement, (iProps)>((props, ref) => {
|
22
|
+
const hiddenFileInput = React.useRef(null);
|
23
|
+
const isMulti = props.allowMultiple === true;
|
24
|
+
return <>
|
25
|
+
{isNullOrEmptyString(props.secondaryContent)
|
26
|
+
? <ButtonEX ref={ref} {...(props.buttonProps || {})} icon={props.icon} showTitleWithIcon={props.showTitleWithIcon} onClick={() => {
|
27
|
+
hiddenFileInput.current.value = "";
|
28
|
+
hiddenFileInput.current.click();
|
29
|
+
}} title={props.title}
|
30
|
+
disabled={props.disabled}
|
31
|
+
/>
|
32
|
+
: <CompoundButtonEXSecondary ref={ref} {...(props.buttonProps || {})} icon={props.icon}
|
33
|
+
secondaryContent={props.secondaryContent}
|
34
|
+
onClick={() => {
|
35
|
+
hiddenFileInput.current.value = "";
|
36
|
+
hiddenFileInput.current.click();
|
37
|
+
}} title={props.title}
|
38
|
+
disabled={props.disabled}
|
39
|
+
/>}
|
40
|
+
<input type="file" ref={hiddenFileInput} style={{ display: "none" }} multiple={isMulti}
|
41
|
+
accept={isNotEmptyArray(props.limitFileTypes) ? props.limitFileTypes.map(ft => `.${ft}`).join() : undefined}
|
42
|
+
onChange={(e) => {
|
43
|
+
if (e.target.files && e.target.files.length > 0) {
|
44
|
+
if (isMulti) {
|
45
|
+
if (isFunction(props.onChange)) {
|
46
|
+
props.onChange(e.target.files);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
else {
|
50
|
+
const fileUploaded = e.target.files && e.target.files[0];
|
51
|
+
if (isFunction(props.onChange)) {
|
52
|
+
props.onChange(fileUploaded);
|
53
|
+
}
|
54
|
+
if (isFunction(props.asBase64) && fileUploaded) {
|
55
|
+
const reader = new FileReader();
|
56
|
+
reader.onloadend = () => {
|
57
|
+
console.log(reader.result);
|
58
|
+
if (!isNullOrEmptyString(reader.result))
|
59
|
+
props.asBase64(reader.result as string);
|
60
|
+
};
|
61
|
+
reader.readAsDataURL(fileUploaded);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}}
|
66
|
+
/>
|
67
|
+
</>;
|
68
|
+
});
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { makeStyles } from '@fluentui/react-components';
|
2
|
+
import { isNotEmptyArray } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { KnownClassNames, mixins } from '../styles/styles';
|
5
|
+
import { ISectionProps, Section } from './section';
|
6
|
+
|
7
|
+
const useStyles = makeStyles({
|
8
|
+
horizontal: {
|
9
|
+
...mixins.flex,
|
10
|
+
flexDirection: 'row'
|
11
|
+
},
|
12
|
+
wrap: mixins.wrap,
|
13
|
+
nogap: mixins.nogap
|
14
|
+
})
|
15
|
+
|
16
|
+
interface IProps extends ISectionProps {
|
17
|
+
wrap?: boolean;
|
18
|
+
nogap?: boolean;
|
19
|
+
}
|
20
|
+
export const Horizontal: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
|
21
|
+
const cssNames = useStyles();
|
22
|
+
let css: string[] = [KnownClassNames.horizontal];
|
23
|
+
|
24
|
+
css.push(cssNames.horizontal);
|
25
|
+
if (props.wrap)
|
26
|
+
css.push(cssNames.wrap);
|
27
|
+
if (props.nogap)
|
28
|
+
css.push(cssNames.nogap);
|
29
|
+
|
30
|
+
if (isNotEmptyArray(props.css)) css.push(...props.css);
|
31
|
+
|
32
|
+
return (
|
33
|
+
<Section {...props} css={css} />
|
34
|
+
);
|
35
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import { GriffelStyle, Input, InputProps, makeStyles, mergeClasses, Textarea, TextareaProps } from '@fluentui/react-components';
|
2
|
+
import { isFunction } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
|
5
|
+
|
6
|
+
interface IProps extends InputProps {
|
7
|
+
onOK?: () => void;
|
8
|
+
onCancel?: () => void;
|
9
|
+
}
|
10
|
+
export const InputEx: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
|
11
|
+
return (
|
12
|
+
<Input appearance='underline' {...props}
|
13
|
+
onKeyDown={isFunction(props.onOK) || isFunction(props.onCancel)
|
14
|
+
? e => {
|
15
|
+
if (isFunction(props.onOK) && e.key === "Enter") props.onOK();
|
16
|
+
else if (isFunction(props.onCancel) && e.key === "Escape") props.onCancel();
|
17
|
+
}
|
18
|
+
: undefined
|
19
|
+
}
|
20
|
+
/>
|
21
|
+
);
|
22
|
+
}
|
23
|
+
|
24
|
+
const fullSize: GriffelStyle = {
|
25
|
+
width: '100% !important',
|
26
|
+
maxHeight: '100% !important'
|
27
|
+
};
|
28
|
+
const useStyles = makeStyles({
|
29
|
+
fullSizeTextArea: {
|
30
|
+
...fullSize,
|
31
|
+
['& > textarea']: fullSize
|
32
|
+
},
|
33
|
+
})
|
34
|
+
|
35
|
+
interface IPropsTextArea extends TextareaProps {
|
36
|
+
fullSize?: boolean;
|
37
|
+
growNoShrink?: boolean;
|
38
|
+
}
|
39
|
+
export const TextAreaEx: React.FunctionComponent<React.PropsWithChildren<IPropsTextArea>> = (props) => {
|
40
|
+
const cssNames = useStyles();
|
41
|
+
let css: string[] = [];
|
42
|
+
|
43
|
+
if (props.fullSize) css.push(cssNames.fullSizeTextArea);
|
44
|
+
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
45
|
+
const recalcHeight = React.useCallback(() => {
|
46
|
+
if (textAreaRef.current && props.growNoShrink) {
|
47
|
+
if (textAreaRef.current.scrollHeight > textAreaRef.current.clientHeight)
|
48
|
+
textAreaRef.current.style.minHeight = textAreaRef.current.scrollHeight + 'px';
|
49
|
+
}
|
50
|
+
}, [textAreaRef]);
|
51
|
+
|
52
|
+
let style: React.CSSProperties = { height: '100%', ...props.style };
|
53
|
+
return (
|
54
|
+
<Textarea ref={textAreaRef} className={mergeClasses(...css)} {...props} style={style} onChange={(e, d) => {
|
55
|
+
if (props.onChange) props.onChange(e, d);
|
56
|
+
recalcHeight();
|
57
|
+
}} />
|
58
|
+
);
|
59
|
+
}
|
@@ -0,0 +1,118 @@
|
|
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 { KnownClassNames, mixins } from '../styles/styles';
|
5
|
+
import { Horizontal } from './horizontal';
|
6
|
+
import { Section } from './section';
|
7
|
+
import { Vertical } from './vertical';
|
8
|
+
|
9
|
+
const useStyles = makeStyles({
|
10
|
+
list: {
|
11
|
+
rowGap: 0
|
12
|
+
},
|
13
|
+
listItem: {
|
14
|
+
padding: tokens.spacingVerticalS,
|
15
|
+
':hover': {
|
16
|
+
backgroundColor: tokens.colorNeutralBackground1Hover
|
17
|
+
}
|
18
|
+
},
|
19
|
+
listItemSelected: {
|
20
|
+
backgroundColor: tokens.colorNeutralBackground1Selected
|
21
|
+
},
|
22
|
+
media: {
|
23
|
+
width: '32px',
|
24
|
+
fontSize: tokens.fontSizeBase600,
|
25
|
+
display: 'flex',
|
26
|
+
flexDirection: 'column',
|
27
|
+
justifyContent: 'center'
|
28
|
+
},
|
29
|
+
image: {
|
30
|
+
width: tokens.lineHeightBase600,
|
31
|
+
height: tokens.lineHeightBase600,
|
32
|
+
backgroundPosition: 'center center',
|
33
|
+
backgroundSize: 'cover',
|
34
|
+
borderRadius: tokens.borderRadiusCircular,
|
35
|
+
border: `1px solid ${tokens.colorNeutralStroke1}`
|
36
|
+
},
|
37
|
+
listItemBody: {
|
38
|
+
rowGap: 0,
|
39
|
+
width: 'calc(100% - 44px)'
|
40
|
+
},
|
41
|
+
listItemHeader: mixins.ellipsis,
|
42
|
+
listItemContent: {
|
43
|
+
...mixins.ellipsis,
|
44
|
+
fontSize: tokens.fontSizeBase200
|
45
|
+
},
|
46
|
+
listItemMedia: {
|
47
|
+
...mixins.ellipsis,
|
48
|
+
maxWidth: '20%',
|
49
|
+
'& svg': {
|
50
|
+
height: tokens.fontSizeBase300
|
51
|
+
},
|
52
|
+
'& button': {
|
53
|
+
padding: 0,
|
54
|
+
minWidth: 0,
|
55
|
+
minHeight: 0,
|
56
|
+
height: '14px'
|
57
|
+
}
|
58
|
+
},
|
59
|
+
listItemMediaNoTrim: {
|
60
|
+
overflow: 'visible',
|
61
|
+
maxWidth: 'fit-content'
|
62
|
+
},
|
63
|
+
listItemMultilineContent: {
|
64
|
+
whiteSpace: 'pre-line'
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
export interface iListItem {
|
69
|
+
key: string | number;
|
70
|
+
media?: JSX.Element | string;
|
71
|
+
header: string;
|
72
|
+
headerMedia?: JSX.Element | string;
|
73
|
+
content?: string | JSX.Element | (string | JSX.Element)[];
|
74
|
+
onClickOnMedia?: boolean;
|
75
|
+
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
76
|
+
selected?: boolean;
|
77
|
+
}
|
78
|
+
interface IProps {
|
79
|
+
selectable?: boolean;
|
80
|
+
items: iListItem[];
|
81
|
+
showAllHeaderMedia?: boolean;
|
82
|
+
/** allow multiline content */
|
83
|
+
multiline?: boolean;
|
84
|
+
dark?: boolean;
|
85
|
+
}
|
86
|
+
|
87
|
+
export const ListEx = (props: IProps) => {
|
88
|
+
|
89
|
+
const cssNames = useStyles();
|
90
|
+
|
91
|
+
const listItemElm = (item: iListItem) => <Horizontal key={item.key} css={[cssNames.listItem, item.selected && cssNames.listItemSelected]} onClick={item.onClick}>
|
92
|
+
{item.media && <Section css={[cssNames.media]} onClick={(e) => {
|
93
|
+
if (!item.onClickOnMedia)
|
94
|
+
e.stopPropagation();//media may have its on onclick
|
95
|
+
}}>{
|
96
|
+
isString(item.media)
|
97
|
+
? <div className={cssNames.image} style={{ backgroundImage: `url('${encodeURI(item.media)}'), url('${props.dark ? LOGO_WHITE_SQUARE : LOGO_BLUE_SQUARE}')` }}></div>
|
98
|
+
: item.media
|
99
|
+
}</Section>}
|
100
|
+
<Vertical main css={[cssNames.listItemBody]}>
|
101
|
+
<Horizontal main>
|
102
|
+
<Section main css={[cssNames.listItemHeader]}>{item.header}</Section>
|
103
|
+
{item.headerMedia && <Section onClick={(e) => {
|
104
|
+
e.stopPropagation();//media may have its on onclick
|
105
|
+
}} css={[cssNames.listItemMedia, props.showAllHeaderMedia && cssNames.listItemMediaNoTrim]}>{item.headerMedia}</Section>}
|
106
|
+
</Horizontal>
|
107
|
+
{!isNullOrUndefined(item.content)
|
108
|
+
? (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>)
|
109
|
+
: undefined}
|
110
|
+
</Vertical>
|
111
|
+
</Horizontal>;
|
112
|
+
|
113
|
+
return (
|
114
|
+
<Vertical css={[cssNames.list, KnownClassNames.list]}>
|
115
|
+
{props.items.map(item => listItemElm(item))}
|
116
|
+
</Vertical>
|
117
|
+
);
|
118
|
+
}
|
@@ -0,0 +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
|
+
);
|
11
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { Field, ProgressBar } from '@fluentui/react-components';
|
2
|
+
import { isFunction } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { IPrompterProps, Prompter } from './prompt';
|
5
|
+
|
6
|
+
interface IProps {
|
7
|
+
step?: number; max?: number;
|
8
|
+
/** do not wrap in a dialog */
|
9
|
+
contentOnly?: boolean;
|
10
|
+
cancelText?: string;
|
11
|
+
onCancel?: () => void;
|
12
|
+
}
|
13
|
+
export const PleaseWait: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
|
14
|
+
const field = <Field validationMessage="please wait..." validationState="none">
|
15
|
+
<ProgressBar value={props.step} max={props.max} />
|
16
|
+
</Field>;
|
17
|
+
return (props.contentOnly
|
18
|
+
? field
|
19
|
+
: <Prompter hideOk
|
20
|
+
hideCancel={!isFunction(props.onCancel)}
|
21
|
+
cancelButtonText={props.cancelText || 'cancel'}
|
22
|
+
onCancel={props.onCancel}>{field}</Prompter>
|
23
|
+
);
|
24
|
+
}
|
25
|
+
|
26
|
+
export const PleaseWaitPrompt = (props: { message: string; step?: number; max?: number; }): IPrompterProps => ({
|
27
|
+
//title: 'please wait...',
|
28
|
+
hideOk: true, hideCancel: true,
|
29
|
+
children: <Field validationMessage={props.message} validationState="none">
|
30
|
+
<ProgressBar value={props.step} max={props.max} />
|
31
|
+
</Field>
|
32
|
+
});
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import { Dialog, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger } from '@fluentui/react-components';
|
2
|
+
import { isNullOrEmptyString } from '@kwiz/common';
|
3
|
+
import React from 'react';
|
4
|
+
import { ButtonEXProps, ButtonEXSecondary } from './button';
|
5
|
+
|
6
|
+
export interface IPrompterProps {
|
7
|
+
hideOk?: boolean;
|
8
|
+
hideCancel?: boolean;
|
9
|
+
/** return false to prevent closing the dialog. */
|
10
|
+
onOK?: () => Promise<void> | void;
|
11
|
+
onCancel?: () => void;
|
12
|
+
okButtonText?: string;
|
13
|
+
cancelButtonText?: string;
|
14
|
+
title?: string;
|
15
|
+
okButtonProps?: Partial<ButtonEXProps>;
|
16
|
+
cancelButtonProps?: Partial<ButtonEXProps>;
|
17
|
+
children?: JSX.Element;
|
18
|
+
}
|
19
|
+
export const Prompter: React.FunctionComponent<React.PropsWithChildren<IPrompterProps>> = (props) => {
|
20
|
+
let okProps: ButtonEXProps = {
|
21
|
+
...(props.okButtonProps as any || {}),
|
22
|
+
onClick: () => props.onOK(),
|
23
|
+
title: props.okButtonText || 'yes'
|
24
|
+
};
|
25
|
+
let cancelProps: ButtonEXProps = {
|
26
|
+
...(props.cancelButtonProps as any || {}),
|
27
|
+
onClick: () => props.onCancel(),
|
28
|
+
title: props.cancelButtonText || 'no'
|
29
|
+
};
|
30
|
+
React.useEffect(() => {
|
31
|
+
let handler = (e: KeyboardEvent) => {
|
32
|
+
if (e.key === "Enter") props.onOK();
|
33
|
+
else if (e.key === "Escape") props.onCancel();
|
34
|
+
|
35
|
+
};
|
36
|
+
document.addEventListener("keydown", handler);
|
37
|
+
return () => document.removeEventListener("keydown", handler);
|
38
|
+
});
|
39
|
+
return (
|
40
|
+
<Dialog open>
|
41
|
+
<DialogSurface>
|
42
|
+
{!isNullOrEmptyString(props.title) && <DialogTitle>{props.title}</DialogTitle>}
|
43
|
+
<DialogBody>
|
44
|
+
<DialogContent>
|
45
|
+
{props.children}
|
46
|
+
</DialogContent>
|
47
|
+
<DialogActions>
|
48
|
+
{props.hideOk ? undefined : <DialogTrigger disableButtonEnhancement>
|
49
|
+
<ButtonEXSecondary {...okProps} />
|
50
|
+
</DialogTrigger>}
|
51
|
+
{props.hideCancel ? undefined : <DialogTrigger disableButtonEnhancement>
|
52
|
+
<ButtonEXSecondary {...cancelProps} />
|
53
|
+
</DialogTrigger>}
|
54
|
+
</DialogActions>
|
55
|
+
</DialogBody>
|
56
|
+
</DialogSurface>
|
57
|
+
</Dialog>
|
58
|
+
);
|
59
|
+
}
|