@kwiz/fluentui 1.0.39 → 1.0.41

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/dist/controls/ColorPickerDialog.d.ts +13 -0
  2. package/dist/controls/ColorPickerDialog.js +34 -0
  3. package/dist/controls/ColorPickerDialog.js.map +1 -0
  4. package/dist/controls/canvas/CustomEventTargetBase.d.ts +7 -0
  5. package/dist/controls/canvas/CustomEventTargetBase.js +22 -0
  6. package/dist/controls/canvas/CustomEventTargetBase.js.map +1 -0
  7. package/dist/controls/canvas/DrawPad.d.ts +15 -0
  8. package/dist/controls/canvas/DrawPad.js +151 -0
  9. package/dist/controls/canvas/DrawPad.js.map +1 -0
  10. package/dist/controls/canvas/DrawPadManager.d.ts +84 -0
  11. package/dist/controls/canvas/DrawPadManager.js +478 -0
  12. package/dist/controls/canvas/DrawPadManager.js.map +1 -0
  13. package/dist/controls/canvas/bezier.d.ts +17 -0
  14. package/dist/controls/canvas/bezier.js +65 -0
  15. package/dist/controls/canvas/bezier.js.map +1 -0
  16. package/dist/controls/canvas/point.d.ts +16 -0
  17. package/dist/controls/canvas/point.js +26 -0
  18. package/dist/controls/canvas/point.js.map +1 -0
  19. package/dist/controls/file-upload.d.ts +8 -3
  20. package/dist/controls/file-upload.js +110 -28
  21. package/dist/controls/file-upload.js.map +1 -1
  22. package/dist/controls/kwizoverflow.js +3 -1
  23. package/dist/controls/kwizoverflow.js.map +1 -1
  24. package/dist/helpers/drag-drop/exports.d.ts +8 -0
  25. package/dist/helpers/drag-drop/exports.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/package.json +3 -2
  30. package/src/controls/ColorPickerDialog.tsx +76 -0
  31. package/src/controls/canvas/CustomEventTargetBase.ts +33 -0
  32. package/src/controls/canvas/DrawPad.tsx +195 -0
  33. package/src/controls/canvas/DrawPadManager.ts +668 -0
  34. package/src/controls/canvas/bezier.ts +110 -0
  35. package/src/controls/canvas/point.ts +45 -0
  36. package/src/controls/file-upload.tsx +117 -36
  37. package/src/controls/kwizoverflow.tsx +5 -2
  38. package/src/helpers/drag-drop/exports.ts +11 -1
  39. package/src/index.ts +2 -0
@@ -0,0 +1,110 @@
1
+ import { BasicPoint, Point } from './point';
2
+
3
+ export class Bezier {
4
+ public static fromPoints(
5
+ points: Point[],
6
+ widths: { start: number; end: number; },
7
+ ): Bezier {
8
+ const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
9
+ const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
10
+
11
+ return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
12
+ }
13
+
14
+ private static calculateControlPoints(
15
+ s1: BasicPoint,
16
+ s2: BasicPoint,
17
+ s3: BasicPoint,
18
+ ): {
19
+ c1: BasicPoint;
20
+ c2: BasicPoint;
21
+ } {
22
+ const dx1 = s1.x - s2.x;
23
+ const dy1 = s1.y - s2.y;
24
+ const dx2 = s2.x - s3.x;
25
+ const dy2 = s2.y - s3.y;
26
+
27
+ const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
28
+ const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
29
+
30
+ const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
31
+ const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
32
+
33
+ const dxm = m1.x - m2.x;
34
+ const dym = m1.y - m2.y;
35
+
36
+ const k = (l1 + l2) === 0 ? l2 : l2 / (l1 + l2);
37
+
38
+ const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
39
+
40
+ const tx = s2.x - cm.x;
41
+ const ty = s2.y - cm.y;
42
+
43
+ return {
44
+ c1: new Point(m1.x + tx, m1.y + ty),
45
+ c2: new Point(m2.x + tx, m2.y + ty),
46
+ };
47
+ }
48
+
49
+ public constructor(
50
+ public startPoint: Point,
51
+ public control2: BasicPoint,
52
+ public control1: BasicPoint,
53
+ public endPoint: Point,
54
+ public startWidth: number,
55
+ public endWidth: number,
56
+ ) { }
57
+
58
+ // Returns approximated length. Code taken from https://www.lemoda.net/maths/bezier-length/index.html.
59
+ public length(): number {
60
+ const steps = 10;
61
+ let length = 0;
62
+ let px;
63
+ let py;
64
+
65
+ for (let i = 0; i <= steps; i += 1) {
66
+ const t = i / steps;
67
+ const cx = this.point(
68
+ t,
69
+ this.startPoint.x,
70
+ this.control1.x,
71
+ this.control2.x,
72
+ this.endPoint.x,
73
+ );
74
+ const cy = this.point(
75
+ t,
76
+ this.startPoint.y,
77
+ this.control1.y,
78
+ this.control2.y,
79
+ this.endPoint.y,
80
+ );
81
+
82
+ if (i > 0) {
83
+ const xdiff = cx - (px as number);
84
+ const ydiff = cy - (py as number);
85
+
86
+ length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
87
+ }
88
+
89
+ px = cx;
90
+ py = cy;
91
+ }
92
+
93
+ return length;
94
+ }
95
+
96
+ // Calculate parametric value of x or y given t and the four point coordinates of a cubic bezier curve.
97
+ private point(
98
+ t: number,
99
+ start: number,
100
+ c1: number,
101
+ c2: number,
102
+ end: number,
103
+ ): number {
104
+ // prettier-ignore
105
+ return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
106
+ + (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
107
+ + (3.0 * c2 * (1.0 - t) * t * t)
108
+ + (end * t * t * t);
109
+ }
110
+ }
@@ -0,0 +1,45 @@
1
+ // Interface for point data structure used e.g. in SignaturePad#fromData method
2
+ export interface BasicPoint {
3
+ x: number;
4
+ y: number;
5
+ pressure: number;
6
+ time: number;
7
+ }
8
+
9
+ export class Point implements BasicPoint {
10
+ public x: number;
11
+ public y: number;
12
+ public pressure: number;
13
+ public time: number;
14
+
15
+ public constructor(x: number, y: number, pressure?: number, time?: number) {
16
+ if (isNaN(x) || isNaN(y)) {
17
+ throw new Error(`Point is invalid: (${x}, ${y})`);
18
+ }
19
+ this.x = +x;
20
+ this.y = +y;
21
+ this.pressure = pressure || 0;
22
+ this.time = time || Date.now();
23
+ }
24
+
25
+ public distanceTo(start: BasicPoint): number {
26
+ return Math.sqrt(
27
+ Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2),
28
+ );
29
+ }
30
+
31
+ public equals(other: BasicPoint): boolean {
32
+ return (
33
+ this.x === other.x &&
34
+ this.y === other.y &&
35
+ this.pressure === other.pressure &&
36
+ this.time === other.time
37
+ );
38
+ }
39
+
40
+ public velocityFrom(start: BasicPoint): number {
41
+ return this.time !== start.time
42
+ ? this.distanceTo(start) / (this.time - start.time)
43
+ : 0;
44
+ }
45
+ }
@@ -1,8 +1,19 @@
1
- import { ButtonProps } from "@fluentui/react-components";
2
- import { isFunction, isNotEmptyArray, isNullOrEmptyString } from '@kwiz/common';
1
+ import { ButtonProps, makeStyles, shorthands, tokens } from "@fluentui/react-components";
2
+ import { ArrowUploadRegular } from "@fluentui/react-icons";
3
+ import { isFunction, isNotEmptyArray, isNotEmptyString, isNullOrEmptyString, lastOrNull } from '@kwiz/common';
3
4
  import * as React from "react";
5
+ import { useDragDropContext } from "../helpers/drag-drop/drag-drop-context";
6
+ import { dropFiles } from "../helpers/drag-drop/exports";
7
+ import { useEffectOnlyOnMount } from "../helpers/hooks";
4
8
  import { ButtonEX, CompoundButtonEXSecondary } from "./button";
5
9
 
10
+ const useStyles = makeStyles({
11
+ addRowIsOver: {
12
+ ...shorthands.borderColor(tokens.colorBrandBackground)
13
+ }
14
+ });
15
+
16
+ type base64Result = { base64: string, filename: string };
6
17
  interface iProps {
7
18
  showTitleWithIcon?: boolean;
8
19
  title?: string;
@@ -11,58 +22,128 @@ interface iProps {
11
22
  limitFileTypes?: string[];
12
23
  allowMultiple?: boolean;
13
24
  icon?: JSX.Element;
14
- onChange?: (newFile: File | FileList) => void;
15
- /** only works for single file, reads it as base64 */
16
- asBase64?: (base64: string) => void;
25
+ onChange?: (newFile: File | File[], errors: string[]) => void;
26
+ asBase64?: (files: base64Result[], errors: string[]) => void;
17
27
  buttonProps?: ButtonProps;
18
28
  disabled?: boolean;
29
+ /** limit file size in MB, for the asBase64 */
30
+ fileSizeLimit?: number;
19
31
  }
20
32
 
21
33
  export const FileUpload = React.forwardRef<HTMLButtonElement, (iProps)>((props, ref) => {
34
+ const classes = useStyles();
22
35
  const hiddenFileInput = React.useRef(null);
23
36
  const isMulti = props.allowMultiple === true;
37
+ const icon = props.icon || <ArrowUploadRegular />;
38
+ const title = isNotEmptyString(props.title) ? props.title : `Drop or select ${isMulti ? 'files' : 'file'}`;
39
+
40
+ const onGotFiles = React.useCallback(async (rawFiles: FileList) => {
41
+ let errors: string[] = [];
42
+ let acceptedFiles: File[] = [];
43
+ if (rawFiles && rawFiles.length > 0) {
44
+ //filter by types and size
45
+ for (let i = 0; i < (isMulti ? rawFiles.length : 1); i++) {
46
+ const currentFile = rawFiles[i];
47
+ let hadError = false;
48
+ if (props.fileSizeLimit > 0) {
49
+ const megabytes = currentFile.size / (1024 * 1024);
50
+ if (megabytes > props.fileSizeLimit) {
51
+ errors.push(`File ${currentFile.name} is over the size limit`);
52
+ hadError = true;
53
+ }
54
+ }
55
+ if (!hadError) {
56
+ if (isNotEmptyArray(props.limitFileTypes)) {
57
+ let fileType = lastOrNull(currentFile.name.split('.')).toLowerCase();
58
+ if (props.limitFileTypes.indexOf(fileType) < 0) {
59
+ errors.push(`File ${currentFile.name} is not allowed`);
60
+ hadError = true;
61
+ }
62
+ }
63
+ }
64
+ if (!hadError) acceptedFiles.push(currentFile);
65
+ }
66
+ }
67
+
68
+ if (isMulti) {
69
+ if (isFunction(props.onChange)) {
70
+ props.onChange(acceptedFiles, errors);
71
+ }
72
+ }
73
+ else {
74
+ const fileUploaded = acceptedFiles[0];
75
+ if (isFunction(props.onChange)) {
76
+ props.onChange(fileUploaded, errors);
77
+ }
78
+ }
79
+
80
+ if (isFunction(props.asBase64)) {
81
+ const filesAs64: base64Result[] = [];
82
+ for (let i = 0; i < (isMulti ? acceptedFiles.length : 1); i++) {
83
+ const currentFile = acceptedFiles[i];
84
+ let hadError = false;
85
+ if (props.fileSizeLimit > 0) {
86
+ const megabytes = currentFile.size / (1024 * 1024);
87
+ if (megabytes > props.fileSizeLimit) {
88
+ errors.push(`File ${currentFile.name} is over the size limit`);
89
+ hadError = true;
90
+ }
91
+ }
92
+ if (!hadError) {
93
+ let as64 = await getFileAsBase64(acceptedFiles[i]);
94
+ if (as64) filesAs64.push(as64);
95
+ else errors.push(`Could not read file ${acceptedFiles[i].name}`);
96
+ }
97
+ }
98
+ props.asBase64(filesAs64, errors);
99
+ }
100
+ }, useEffectOnlyOnMount);
101
+
102
+ const dropContext = useDragDropContext<never, dropFiles>({
103
+ dropInfo: {
104
+ acceptTypes: ["__NATIVE_FILE__"],
105
+ onItemDrop: item => {
106
+ onGotFiles(item.files);
107
+ }
108
+ }
109
+ });
110
+
24
111
  return <>
25
112
  {isNullOrEmptyString(props.secondaryContent)
26
- ? <ButtonEX ref={ref} {...(props.buttonProps || {})} icon={props.icon} showTitleWithIcon={props.showTitleWithIcon} onClick={() => {
113
+ ? <ButtonEX ref={ref || dropContext.dragDropRef} {...(props.buttonProps || {})} icon={icon} showTitleWithIcon={props.showTitleWithIcon} onClick={() => {
27
114
  hiddenFileInput.current.value = "";
28
115
  hiddenFileInput.current.click();
29
- }} title={props.title}
30
- disabled={props.disabled}
116
+ }}
117
+ title={title} disabled={props.disabled}
118
+ className={dropContext.drop.isOver && classes.addRowIsOver}
31
119
  />
32
- : <CompoundButtonEXSecondary ref={ref} {...(props.buttonProps || {})} icon={props.icon}
120
+ : <CompoundButtonEXSecondary ref={ref || dropContext.dragDropRef} {...(props.buttonProps || {})} icon={icon}
33
121
  secondaryContent={props.secondaryContent}
34
122
  onClick={() => {
35
123
  hiddenFileInput.current.value = "";
36
124
  hiddenFileInput.current.click();
37
- }} title={props.title}
38
- disabled={props.disabled}
125
+ }}
126
+ title={title} disabled={props.disabled}
127
+ className={dropContext.drop.isOver && classes.addRowIsOver}
39
128
  />}
40
129
  <input type="file" ref={hiddenFileInput} style={{ display: "none" }} multiple={isMulti}
41
130
  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
- }}
131
+ onChange={async (e) => onGotFiles(e.target.files)}
66
132
  />
67
133
  </>;
68
- });
134
+ });
135
+
136
+ async function getFileAsBase64(file: File): Promise<base64Result> {
137
+ return new Promise<base64Result>(resolve => {
138
+ const reader = new FileReader();
139
+ reader.onloadend = () => {
140
+ if (!isNullOrEmptyString(reader.result))
141
+ resolve({ filename: file.name, base64: reader.result as string });
142
+ else {
143
+ console.warn("Empty file selected");
144
+ resolve(null);
145
+ }
146
+ };
147
+ reader.readAsDataURL(file);
148
+ });
149
+ }
@@ -4,6 +4,7 @@ import {
4
4
  } from "@fluentui/react-components";
5
5
  import { MoreHorizontalFilled } from "@fluentui/react-icons";
6
6
  import { isNumber } from '@kwiz/common';
7
+ import { useKWIZFluentContext } from "../helpers/context";
7
8
 
8
9
  interface IProps<ItemType> {
9
10
  /** you cannot have a menu with trigger in overflow items. put those in groupWrapper controls before/after rendering children. */
@@ -18,6 +19,8 @@ interface IProps<ItemType> {
18
19
  className?: string;
19
20
  }
20
21
  const OverflowMenu = <ItemType,>(props: IProps<ItemType>) => {
22
+ const ctx = useKWIZFluentContext();
23
+
21
24
  const { ref, isOverflowing, overflowCount } =
22
25
  useOverflowMenu<HTMLButtonElement>();
23
26
 
@@ -25,12 +28,12 @@ const OverflowMenu = <ItemType,>(props: IProps<ItemType>) => {
25
28
  return null;
26
29
  }
27
30
 
28
- let menu = <Menu>
31
+ let menu = <Menu mountNode={ctx.mountNode}>
29
32
  <MenuTrigger disableButtonEnhancement>
30
33
  {props.menuTrigger
31
34
  ? props.menuTrigger(props.menuRef || ref, overflowCount)
32
35
  : <MenuButton
33
- icon={<MoreHorizontalFilled/>}
36
+ icon={<MoreHorizontalFilled />}
34
37
  ref={props.menuRef || ref}
35
38
  aria-label="More items"
36
39
  appearance="subtle"
@@ -1,4 +1,14 @@
1
+ import { NativeTypes } from 'react-dnd-html5-backend';
2
+ import { iDraggedItemType } from './use-draggable';
3
+ import { iDroppableProps } from './use-droppable';
4
+
1
5
  export { DragDropContainer } from './drag-drop-container';
2
6
  export { DragDropContextProvider, useDragDropContext } from "./drag-drop-context";
3
7
  export type { iDraggedItemType } from "./use-draggable";
4
- export type { iDroppableProps } from "./use-droppable";
8
+ export type { iDroppableProps } from "./use-droppable";
9
+
10
+ type fileNativeType = typeof NativeTypes.FILE;
11
+ interface dragFiles extends iDraggedItemType<fileNativeType> {
12
+ files: FileList;
13
+ }
14
+ export type dropFiles = iDroppableProps<fileNativeType, dragFiles>;
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from './controls/accordion';
2
2
  export * from './controls/button';
3
+ export * from './controls/canvas/DrawPad';
3
4
  export * from './controls/centered';
5
+ export * from './controls/ColorPickerDialog';
4
6
  export * from './controls/date';
5
7
  export * from './controls/divider';
6
8
  export * from './controls/dropdown';