@kwiz/fluentui 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|