@etsoo/react 1.5.23 → 1.5.26

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/lib/index.d.ts CHANGED
@@ -88,6 +88,7 @@ export * from './states/RouterProps';
88
88
  export * from './states/State';
89
89
  export * from './states/UserState';
90
90
  export * from './uses/useCombinedRefs';
91
+ export * from './uses/useDelayedExecutor';
91
92
  export * from './uses/useDimensions';
92
93
  export * from './uses/useTimeout';
93
94
  export * from './uses/useWindowSize';
package/lib/index.js CHANGED
@@ -94,6 +94,7 @@ export * from './states/State';
94
94
  export * from './states/UserState';
95
95
  // uses
96
96
  export * from './uses/useCombinedRefs';
97
+ export * from './uses/useDelayedExecutor';
97
98
  export * from './uses/useDimensions';
98
99
  export * from './uses/useTimeout';
99
100
  export * from './uses/useWindowSize';
@@ -15,7 +15,9 @@ export function BridgeCloseButton(props) {
15
15
  : globalApp.get('close'), validate, ...rest } = props;
16
16
  // Host
17
17
  const host = BridgeUtils.host;
18
- if (host == null || (validate && validate(host) === false)) {
18
+ if (host == null ||
19
+ !host.closable() ||
20
+ (validate && validate(host) === false)) {
19
21
  return React.createElement(React.Fragment, null);
20
22
  }
21
23
  // Click handler
@@ -1,5 +1,6 @@
1
1
  import { TextField } from '@mui/material';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
3
4
  import { MUGlobal } from './MUGlobal';
4
5
  /**
5
6
  * Input field
@@ -15,7 +16,9 @@ export function InputField(props) {
15
16
  if (readOnly != null)
16
17
  InputProps.readOnly = readOnly;
17
18
  const isMounted = React.useRef(true);
18
- const delaySeed = React.useRef(0);
19
+ const delayed = onChange != null && changeDelay != null && changeDelay >= 1
20
+ ? useDelayedExecutor(onChange, changeDelay)
21
+ : undefined;
19
22
  const onChangeEx = (event) => {
20
23
  if (onChange == null)
21
24
  return;
@@ -23,17 +26,12 @@ export function InputField(props) {
23
26
  onChange(event);
24
27
  return;
25
28
  }
26
- if (delaySeed.current > 0)
27
- window.clearTimeout(delaySeed.current);
28
- delaySeed.current = window.setTimeout(() => {
29
- if (isMounted.current)
30
- onChange(event);
31
- }, changeDelay);
29
+ delayed === null || delayed === void 0 ? void 0 : delayed.call(undefined, event);
32
30
  };
33
31
  React.useEffect(() => {
34
32
  return () => {
35
33
  isMounted.current = false;
36
- window.clearTimeout(delaySeed.current);
34
+ delayed === null || delayed === void 0 ? void 0 : delayed.clear();
37
35
  };
38
36
  }, []);
39
37
  // Layout
@@ -5,6 +5,7 @@ import { useDimensions } from '../uses/useDimensions';
5
5
  import { DomUtils } from '@etsoo/shared';
6
6
  import { Labels } from '../app/Labels';
7
7
  import { ReactUtils } from '../app/ReactUtils';
8
+ import { useDelayedExecutor } from '..';
8
9
  // Cached width attribute name
9
10
  const cachedWidthName = 'data-cached-width';
10
11
  // Reset form
@@ -179,7 +180,7 @@ export function SearchBar(props) {
179
180
  return;
180
181
  if (state.form == null)
181
182
  state.form = event.currentTarget;
182
- handleSubmit();
183
+ delayed.call();
183
184
  };
184
185
  // Handle more button click
185
186
  const handleMore = () => {
@@ -191,8 +192,18 @@ export function SearchBar(props) {
191
192
  return;
192
193
  if (state.moreForm == null)
193
194
  state.moreForm = event.currentTarget;
194
- handleSubmit();
195
+ delayed.call();
195
196
  };
197
+ // Submit at once
198
+ const handleSubmitInstant = (reset = false) => {
199
+ // Prepare data
200
+ const data = new FormData(state.form);
201
+ if (state.moreForm != null) {
202
+ DomUtils.mergeFormData(data, new FormData(state.moreForm));
203
+ }
204
+ onSubmit(data, reset);
205
+ };
206
+ const delayed = useDelayedExecutor(handleSubmitInstant, 480);
196
207
  // Reset
197
208
  const handleReset = () => {
198
209
  // Clear form values
@@ -203,36 +214,12 @@ export function SearchBar(props) {
203
214
  // Resubmit
204
215
  handleSubmitInstant(true);
205
216
  };
206
- // Submit
207
- const handleSubmit = (delay = 480) => {
208
- if (state.submitSeed != null) {
209
- clearTimeout(state.submitSeed);
210
- }
211
- // Delay the change
212
- state.submitSeed = window.setTimeout(handleSubmitInstant, delay);
213
- };
214
- // Submit at once
215
- const handleSubmitInstant = (reset = false) => {
216
- // Reset timeout
217
- state.submitSeed = undefined;
218
- // Prepare data
219
- const data = new FormData(state.form);
220
- if (state.moreForm != null) {
221
- DomUtils.mergeFormData(data, new FormData(state.moreForm));
222
- }
223
- onSubmit(data, reset);
224
- };
225
- // First loading
226
217
  React.useEffect(() => {
218
+ // Delayed way
219
+ delayed.call(100);
227
220
  return () => {
228
- // Clear the seeds
229
- if (state.submitSeed != null)
230
- clearTimeout(state.submitSeed);
221
+ delayed.clear();
231
222
  };
232
- }, []);
233
- React.useEffect(() => {
234
- // Delayed way
235
- handleSubmit(100);
236
223
  }, [className]);
237
224
  // Layout
238
225
  return (React.createElement(React.Fragment, null,
@@ -1,5 +1,6 @@
1
1
  import { TextField } from '@mui/material';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
3
4
  import { MUGlobal } from './MUGlobal';
4
5
  /**
5
6
  * Search field
@@ -15,7 +16,9 @@ export function SearchField(props) {
15
16
  if (readOnly != null)
16
17
  InputProps.readOnly = readOnly;
17
18
  const isMounted = React.useRef(true);
18
- const delaySeed = React.useRef(0);
19
+ const delayed = onChange != null && changeDelay != null && changeDelay >= 1
20
+ ? useDelayedExecutor(onChange, changeDelay)
21
+ : undefined;
19
22
  const onChangeEx = (event) => {
20
23
  if (onChange == null)
21
24
  return;
@@ -23,17 +26,12 @@ export function SearchField(props) {
23
26
  onChange(event);
24
27
  return;
25
28
  }
26
- if (delaySeed.current > 0)
27
- window.clearTimeout(delaySeed.current);
28
- delaySeed.current = window.setTimeout(() => {
29
- if (isMounted.current)
30
- onChange(event);
31
- }, changeDelay);
29
+ delayed === null || delayed === void 0 ? void 0 : delayed.call(undefined, event);
32
30
  };
33
31
  React.useEffect(() => {
34
32
  return () => {
35
33
  isMounted.current = false;
36
- window.clearTimeout(delaySeed.current);
34
+ delayed === null || delayed === void 0 ? void 0 : delayed.clear();
37
35
  };
38
36
  }, []);
39
37
  // Layout
@@ -4,6 +4,7 @@ import { MUGlobal } from './MUGlobal';
4
4
  import { Clear, Visibility } from '@mui/icons-material';
5
5
  import useCombinedRefs from '../uses/useCombinedRefs';
6
6
  import { Keyboard } from '@etsoo/shared';
7
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
7
8
  export const TextFieldEx = React.forwardRef((props, ref) => {
8
9
  // Destructure
9
10
  const { changeDelay, error, fullWidth = true, helperText, InputProps = {}, onChange, onKeyPress, onEnter, inputRef, readOnly, showClear, showPassword, type, variant = MUGlobal.textFieldVariant, ...rest } = props;
@@ -91,7 +92,9 @@ export const TextFieldEx = React.forwardRef((props, ref) => {
91
92
  }
92
93
  }), []);
93
94
  const isMounted = React.useRef(true);
94
- const delaySeed = React.useRef(0);
95
+ const delayed = onChange != null && changeDelay != null && changeDelay >= 1
96
+ ? useDelayedExecutor(onChange, changeDelay)
97
+ : undefined;
95
98
  const onChangeEx = (event) => {
96
99
  if (errorText != null) {
97
100
  // Reset
@@ -111,17 +114,12 @@ export const TextFieldEx = React.forwardRef((props, ref) => {
111
114
  onChange(event);
112
115
  return;
113
116
  }
114
- if (delaySeed.current > 0)
115
- window.clearTimeout(delaySeed.current);
116
- delaySeed.current = window.setTimeout(() => {
117
- if (isMounted.current)
118
- onChange(event);
119
- }, changeDelay);
117
+ delayed === null || delayed === void 0 ? void 0 : delayed.call(undefined, event);
120
118
  };
121
119
  React.useEffect(() => {
122
120
  return () => {
123
121
  isMounted.current = false;
124
- window.clearTimeout(delaySeed.current);
122
+ delayed === null || delayed === void 0 ? void 0 : delayed.clear();
125
123
  };
126
124
  }, []);
127
125
  // Textfield
package/lib/mu/Tiplist.js CHANGED
@@ -2,6 +2,7 @@ import { DataTypes } from '@etsoo/shared';
2
2
  import { Autocomplete } from '@mui/material';
3
3
  import React from 'react';
4
4
  import { ReactUtils } from '../app/ReactUtils';
5
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
5
6
  import { InputField } from './InputField';
6
7
  import { SearchField } from './SearchField';
7
8
  /**
@@ -57,17 +58,11 @@ export function Tiplist(props) {
57
58
  }
58
59
  // Stop bubble
59
60
  event.stopPropagation();
60
- // Clear any seed
61
- if (state.loadDataSeed != null) {
62
- clearTimeout(state.loadDataSeed);
63
- }
64
- // Delay 480 for loading data
65
- state.loadDataSeed = window.setTimeout(loadDataDirect, 480, event.currentTarget.value);
61
+ // Call with delay
62
+ delayed.call(undefined, event.currentTarget.value);
66
63
  };
67
64
  // Directly load data
68
65
  const loadDataDirect = (keyword, id) => {
69
- // Reset seed value
70
- state.loadDataSeed = undefined;
71
66
  // Reset options
72
67
  // setOptions([]);
73
68
  if (id == null) {
@@ -96,6 +91,7 @@ export function Tiplist(props) {
96
91
  });
97
92
  });
98
93
  };
94
+ const delayed = useDelayedExecutor(loadDataDirect, 480);
99
95
  const setInputValue = (value) => {
100
96
  var _a;
101
97
  stateUpdate({ value });
@@ -126,11 +122,8 @@ export function Tiplist(props) {
126
122
  }
127
123
  React.useEffect(() => {
128
124
  return () => {
129
- // Clear any seed
130
- if (state.loadDataSeed != null) {
131
- clearTimeout(state.loadDataSeed);
132
- }
133
125
  isMounted.current = false;
126
+ delayed.clear();
134
127
  };
135
128
  }, []);
136
129
  // Layout
@@ -1,5 +1,6 @@
1
1
  import { ClickAwayListener, Tooltip } from '@mui/material';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
3
4
  /**
4
5
  * Tooltip with click visibility
5
6
  * @param props Props
@@ -12,26 +13,19 @@ export function TooltipClick(props) {
12
13
  // State
13
14
  const [localTitle, setTitle] = React.useState(title);
14
15
  const [open, setOpen] = React.useState(false);
15
- const leaveSeed = React.useRef(0);
16
+ const delayed = leaveDelay > 0
17
+ ? useDelayedExecutor(() => setOpen(false), leaveDelay)
18
+ : undefined;
16
19
  // Callback for open the tooltip
17
20
  const openTooltip = (newTitle) => {
18
21
  setOpen(true);
19
22
  if (newTitle)
20
23
  setTitle(newTitle);
21
- if (leaveDelay > 0) {
22
- clearLeaveSeed();
23
- leaveSeed.current = window.setTimeout(() => setOpen(false), leaveDelay);
24
- }
25
- };
26
- const clearLeaveSeed = () => {
27
- if (leaveSeed.current > 0) {
28
- window.clearTimeout(leaveSeed.current);
29
- leaveSeed.current = 0;
30
- }
24
+ delayed === null || delayed === void 0 ? void 0 : delayed.call();
31
25
  };
32
26
  React.useEffect(() => {
33
27
  return () => {
34
- clearLeaveSeed();
28
+ delayed === null || delayed === void 0 ? void 0 : delayed.clear();
35
29
  };
36
30
  }, []);
37
31
  // Layout
@@ -0,0 +1,5 @@
1
+ import { DelayedExecutorType } from '@etsoo/shared';
2
+ /**
3
+ * Create delayed executor
4
+ */
5
+ export declare function useDelayedExecutor<P extends any[]>(func: (...args: P) => void, delayMiliseconcs: number): DelayedExecutorType<P>;
@@ -0,0 +1,11 @@
1
+ import { ExtendUtils } from '@etsoo/shared';
2
+ import React from 'react';
3
+ /**
4
+ * Create delayed executor
5
+ */
6
+ export function useDelayedExecutor(func, delayMiliseconcs) {
7
+ const ref = React.useRef();
8
+ if (ref.current == null)
9
+ ref.current = ExtendUtils.delayedExecutor(func, delayMiliseconcs);
10
+ return ref.current;
11
+ }
@@ -1,7 +1,6 @@
1
1
  interface states {
2
2
  count: number;
3
3
  indices: number[];
4
- seed?: number;
5
4
  }
6
5
  /**
7
6
  * Calculate element(s) dimensions
@@ -1,5 +1,6 @@
1
1
  import { DomUtils } from '@etsoo/shared';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from './useDelayedExecutor';
3
4
  /**
4
5
  * Calculate element(s) dimensions
5
6
  * @param elements Observed elments count
@@ -13,6 +14,37 @@ export function useDimensions(elements, updateCallback, miliseconds = 50, equalC
13
14
  count: 0,
14
15
  indices: []
15
16
  });
17
+ // Dimentions
18
+ const dimensions = React.useRef();
19
+ if (dimensions.current == null) {
20
+ // Init
21
+ const init = [];
22
+ for (let e = 0; e < elements; e++) {
23
+ init.push(((index) => {
24
+ return [
25
+ (instance) => {
26
+ if (instance != null) {
27
+ // Current element
28
+ const currentElement = init[index][1];
29
+ if (currentElement != null) {
30
+ // Same target, return
31
+ if (currentElement == instance)
32
+ return;
33
+ // Cancel observation
34
+ resizeObserver.unobserve(currentElement);
35
+ }
36
+ // Update element
37
+ init[index][1] = instance;
38
+ // Start observe
39
+ resizeObserver.observe(instance);
40
+ }
41
+ }
42
+ ];
43
+ })(e));
44
+ }
45
+ dimensions.current = init;
46
+ }
47
+ const delayed = useDelayedExecutor(setState, miliseconds);
16
48
  // Observer
17
49
  const resizeObserver = new ResizeObserver((entries) => {
18
50
  // Update Rect
@@ -41,18 +73,9 @@ export function useDimensions(elements, updateCallback, miliseconds = 50, equalC
41
73
  });
42
74
  // Update state
43
75
  if (indices.length > 0) {
44
- const update = { count: state.count + 1, indices, seed: undefined };
45
- if (miliseconds > 0) {
46
- if (state.seed != null) {
47
- clearTimeout(state.seed);
48
- }
49
- state.seed = window.setTimeout((localUpdate) => {
50
- setState(localUpdate);
51
- }, miliseconds, update);
52
- }
53
- else {
54
- setState(update);
55
- }
76
+ // Count only for unique update
77
+ const update = { count: state.count + 1, indices };
78
+ delayed.call(undefined, update);
56
79
  }
57
80
  });
58
81
  // Init
@@ -80,18 +103,14 @@ export function useDimensions(elements, updateCallback, miliseconds = 50, equalC
80
103
  ];
81
104
  })(e));
82
105
  }
83
- // Dimensions
84
- const [dimensions] = React.useState(init);
85
106
  // Layout ready
86
107
  React.useEffect(() => {
87
108
  return () => {
88
109
  // Clear the observer
89
110
  resizeObserver.disconnect();
90
- if (state.seed != null) {
91
- clearTimeout(state.seed);
92
- }
111
+ delayed.clear();
93
112
  };
94
113
  }, []);
95
114
  // Return
96
- return { dimensions, state };
115
+ return { dimensions: dimensions.current, state };
97
116
  }
@@ -3,6 +3,6 @@
3
3
  * @param action Action function
4
4
  * @param milliseconds Interval of milliseconds
5
5
  */
6
- export declare const useTimeout: (action: Function, milliseconds: number) => {
6
+ export declare const useTimeout: (action: (...args: any[]) => void, milliseconds: number) => {
7
7
  cancel: () => void;
8
8
  };
@@ -1,3 +1,4 @@
1
+ import { ExtendUtils } from '@etsoo/shared';
1
2
  import React from 'react';
2
3
  /**
3
4
  * For setTimeout to merge actions
@@ -5,26 +6,17 @@ import React from 'react';
5
6
  * @param milliseconds Interval of milliseconds
6
7
  */
7
8
  export const useTimeout = (action, milliseconds) => {
8
- // Time out seed
9
- let seed = 0;
10
- // Cancel function
11
- const cancel = () => {
12
- if (seed > 0) {
13
- clearTimeout(seed);
14
- seed = 0;
15
- }
16
- };
9
+ // Delayed
10
+ const d = ExtendUtils.delayedExecutor(action, milliseconds);
17
11
  // Merge into the life cycle
18
12
  React.useEffect(() => {
19
- seed = window.setTimeout(() => {
20
- action.call(null);
21
- }, milliseconds);
13
+ d.call();
22
14
  return () => {
23
- cancel();
15
+ d.clear();
24
16
  };
25
17
  }, []);
26
18
  // Return cancel method
27
19
  return {
28
- cancel
20
+ cancel: d.clear
29
21
  };
30
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.5.23",
3
+ "version": "1.5.26",
4
4
  "description": "TypeScript ReactJs framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -50,11 +50,11 @@
50
50
  "@emotion/react": "^11.8.1",
51
51
  "@emotion/style": "^0.8.0",
52
52
  "@emotion/styled": "^11.8.1",
53
- "@etsoo/appscript": "^1.2.52",
54
- "@etsoo/notificationbase": "^1.1.1",
55
- "@etsoo/shared": "^1.1.11",
53
+ "@etsoo/appscript": "^1.2.56",
54
+ "@etsoo/notificationbase": "^1.1.2",
55
+ "@etsoo/shared": "^1.1.14",
56
56
  "@mui/icons-material": "^5.4.2",
57
- "@mui/material": "^5.4.2",
57
+ "@mui/material": "^5.4.3",
58
58
  "@reach/router": "^1.3.4",
59
59
  "@types/pica": "^9.0.0",
60
60
  "@types/pulltorefreshjs": "^0.1.5",
@@ -76,19 +76,19 @@
76
76
  "react-window": "^1.8.6"
77
77
  },
78
78
  "devDependencies": {
79
- "@babel/cli": "^7.17.3",
79
+ "@babel/cli": "^7.17.6",
80
80
  "@babel/core": "^7.17.5",
81
81
  "@babel/plugin-transform-runtime": "^7.17.0",
82
82
  "@babel/preset-env": "^7.16.11",
83
83
  "@babel/runtime-corejs3": "^7.17.2",
84
- "@types/jest": "^27.4.0",
84
+ "@types/jest": "^27.4.1",
85
85
  "@types/react-test-renderer": "^17.0.1",
86
- "@typescript-eslint/eslint-plugin": "^5.12.0",
87
- "@typescript-eslint/parser": "^5.12.0",
88
- "eslint": "^8.9.0",
86
+ "@typescript-eslint/eslint-plugin": "^5.12.1",
87
+ "@typescript-eslint/parser": "^5.12.1",
88
+ "eslint": "^8.10.0",
89
89
  "eslint-config-airbnb-base": "^15.0.0",
90
90
  "eslint-plugin-import": "^2.25.4",
91
- "eslint-plugin-react": "^7.28.0",
91
+ "eslint-plugin-react": "^7.29.2",
92
92
  "jest": "^27.5.1",
93
93
  "react-test-renderer": "^17.0.2",
94
94
  "ts-jest": "^27.1.3",
package/src/index.ts CHANGED
@@ -101,6 +101,7 @@ export * from './states/UserState';
101
101
 
102
102
  // uses
103
103
  export * from './uses/useCombinedRefs';
104
+ export * from './uses/useDelayedExecutor';
104
105
  export * from './uses/useDimensions';
105
106
  export * from './uses/useTimeout';
106
107
  export * from './uses/useWindowSize';
@@ -40,7 +40,11 @@ export function BridgeCloseButton(props: BridgeCloseButtonProps) {
40
40
  // Host
41
41
  const host = BridgeUtils.host;
42
42
 
43
- if (host == null || (validate && validate(host) === false)) {
43
+ if (
44
+ host == null ||
45
+ !host.closable() ||
46
+ (validate && validate(host) === false)
47
+ ) {
44
48
  return <React.Fragment />;
45
49
  }
46
50
 
@@ -1,5 +1,6 @@
1
1
  import { TextField, TextFieldProps } from '@mui/material';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
3
4
  import { MUGlobal } from './MUGlobal';
4
5
 
5
6
  /**
@@ -42,7 +43,10 @@ export function InputField(props: InputFieldProps) {
42
43
  if (readOnly != null) InputProps.readOnly = readOnly;
43
44
 
44
45
  const isMounted = React.useRef(true);
45
- const delaySeed = React.useRef(0);
46
+ const delayed =
47
+ onChange != null && changeDelay != null && changeDelay >= 1
48
+ ? useDelayedExecutor(onChange, changeDelay)
49
+ : undefined;
46
50
 
47
51
  const onChangeEx = (
48
52
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
@@ -54,16 +58,13 @@ export function InputField(props: InputFieldProps) {
54
58
  return;
55
59
  }
56
60
 
57
- if (delaySeed.current > 0) window.clearTimeout(delaySeed.current);
58
- delaySeed.current = window.setTimeout(() => {
59
- if (isMounted.current) onChange(event);
60
- }, changeDelay);
61
+ delayed?.call(undefined, event);
61
62
  };
62
63
 
63
64
  React.useEffect(() => {
64
65
  return () => {
65
66
  isMounted.current = false;
66
- window.clearTimeout(delaySeed.current);
67
+ delayed?.clear();
67
68
  };
68
69
  }, []);
69
70
 
@@ -5,6 +5,7 @@ import { useDimensions } from '../uses/useDimensions';
5
5
  import { DomUtils } from '@etsoo/shared';
6
6
  import { Labels } from '../app/Labels';
7
7
  import { ReactUtils } from '../app/ReactUtils';
8
+ import { useDelayedExecutor } from '..';
8
9
 
9
10
  /**
10
11
  * Search bar props
@@ -103,7 +104,6 @@ export function SearchBar(props: SearchBarProps) {
103
104
  const state = React.useRef<{
104
105
  form?: HTMLFormElement;
105
106
  moreForm?: HTMLFormElement;
106
- submitSeed?: number;
107
107
  lastMaxWidth: number;
108
108
  hasMore: boolean;
109
109
  }>({ hasMore: true, lastMaxWidth: 9999 }).current;
@@ -256,7 +256,7 @@ export function SearchBar(props: SearchBarProps) {
256
256
 
257
257
  if (state.form == null) state.form = event.currentTarget;
258
258
 
259
- handleSubmit();
259
+ delayed.call();
260
260
  };
261
261
 
262
262
  // Handle more button click
@@ -270,34 +270,11 @@ export function SearchBar(props: SearchBarProps) {
270
270
 
271
271
  if (state.moreForm == null) state.moreForm = event.currentTarget;
272
272
 
273
- handleSubmit();
274
- };
275
-
276
- // Reset
277
- const handleReset = () => {
278
- // Clear form values
279
- if (state.form != null) resetForm(state.form);
280
- if (state.moreForm != null) resetForm(state.moreForm);
281
-
282
- // Resubmit
283
- handleSubmitInstant(true);
284
- };
285
-
286
- // Submit
287
- const handleSubmit = (delay = 480) => {
288
- if (state.submitSeed != null) {
289
- clearTimeout(state.submitSeed);
290
- }
291
-
292
- // Delay the change
293
- state.submitSeed = window.setTimeout(handleSubmitInstant, delay);
273
+ delayed.call();
294
274
  };
295
275
 
296
276
  // Submit at once
297
277
  const handleSubmitInstant = (reset: boolean = false) => {
298
- // Reset timeout
299
- state.submitSeed = undefined;
300
-
301
278
  // Prepare data
302
279
  const data = new FormData(state.form);
303
280
  if (state.moreForm != null) {
@@ -307,17 +284,25 @@ export function SearchBar(props: SearchBarProps) {
307
284
  onSubmit(data, reset);
308
285
  };
309
286
 
310
- // First loading
311
- React.useEffect(() => {
312
- return () => {
313
- // Clear the seeds
314
- if (state.submitSeed != null) clearTimeout(state.submitSeed);
315
- };
316
- }, []);
287
+ const delayed = useDelayedExecutor(handleSubmitInstant, 480);
288
+
289
+ // Reset
290
+ const handleReset = () => {
291
+ // Clear form values
292
+ if (state.form != null) resetForm(state.form);
293
+ if (state.moreForm != null) resetForm(state.moreForm);
294
+
295
+ // Resubmit
296
+ handleSubmitInstant(true);
297
+ };
317
298
 
318
299
  React.useEffect(() => {
319
300
  // Delayed way
320
- handleSubmit(100);
301
+ delayed.call(100);
302
+
303
+ return () => {
304
+ delayed.clear();
305
+ };
321
306
  }, [className]);
322
307
 
323
308
  // Layout
@@ -1,5 +1,6 @@
1
1
  import { TextField, TextFieldProps } from '@mui/material';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
3
4
  import { MUGlobal } from './MUGlobal';
4
5
 
5
6
  /**
@@ -42,7 +43,10 @@ export function SearchField(props: SearchFieldProps) {
42
43
  if (readOnly != null) InputProps.readOnly = readOnly;
43
44
 
44
45
  const isMounted = React.useRef(true);
45
- const delaySeed = React.useRef(0);
46
+ const delayed =
47
+ onChange != null && changeDelay != null && changeDelay >= 1
48
+ ? useDelayedExecutor(onChange, changeDelay)
49
+ : undefined;
46
50
 
47
51
  const onChangeEx = (
48
52
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
@@ -54,16 +58,13 @@ export function SearchField(props: SearchFieldProps) {
54
58
  return;
55
59
  }
56
60
 
57
- if (delaySeed.current > 0) window.clearTimeout(delaySeed.current);
58
- delaySeed.current = window.setTimeout(() => {
59
- if (isMounted.current) onChange(event);
60
- }, changeDelay);
61
+ delayed?.call(undefined, event);
61
62
  };
62
63
 
63
64
  React.useEffect(() => {
64
65
  return () => {
65
66
  isMounted.current = false;
66
- window.clearTimeout(delaySeed.current);
67
+ delayed?.clear();
67
68
  };
68
69
  }, []);
69
70
 
@@ -9,6 +9,7 @@ import { MUGlobal } from './MUGlobal';
9
9
  import { Clear, Visibility } from '@mui/icons-material';
10
10
  import useCombinedRefs from '../uses/useCombinedRefs';
11
11
  import { Keyboard } from '@etsoo/shared';
12
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
12
13
 
13
14
  /**
14
15
  * Extended text field props
@@ -193,7 +194,10 @@ export const TextFieldEx = React.forwardRef<
193
194
  );
194
195
 
195
196
  const isMounted = React.useRef(true);
196
- const delaySeed = React.useRef(0);
197
+ const delayed =
198
+ onChange != null && changeDelay != null && changeDelay >= 1
199
+ ? useDelayedExecutor(onChange, changeDelay)
200
+ : undefined;
197
201
 
198
202
  const onChangeEx = (
199
203
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
@@ -218,16 +222,13 @@ export const TextFieldEx = React.forwardRef<
218
222
  return;
219
223
  }
220
224
 
221
- if (delaySeed.current > 0) window.clearTimeout(delaySeed.current);
222
- delaySeed.current = window.setTimeout(() => {
223
- if (isMounted.current) onChange(event);
224
- }, changeDelay);
225
+ delayed?.call(undefined, event);
225
226
  };
226
227
 
227
228
  React.useEffect(() => {
228
229
  return () => {
229
230
  isMounted.current = false;
230
- window.clearTimeout(delaySeed.current);
231
+ delayed?.clear();
231
232
  };
232
233
  }, []);
233
234
 
@@ -3,6 +3,7 @@ import { DataTypes } from '@etsoo/shared';
3
3
  import { Autocomplete, AutocompleteRenderInputParams } from '@mui/material';
4
4
  import React from 'react';
5
5
  import { ReactUtils } from '../app/ReactUtils';
6
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
6
7
  import { AutocompleteExtendedProps } from './AutocompleteExtendedProps';
7
8
  import { InputField } from './InputField';
8
9
  import { SearchField } from './SearchField';
@@ -95,7 +96,6 @@ export function Tiplist<T extends {} = IdLabelDto>(props: TiplistProps<T>) {
95
96
 
96
97
  // State
97
98
  const [state] = React.useState<{
98
- loadDataSeed?: number;
99
99
  idLoaded?: boolean;
100
100
  idSet?: boolean;
101
101
  }>({});
@@ -125,24 +125,12 @@ export function Tiplist<T extends {} = IdLabelDto>(props: TiplistProps<T>) {
125
125
  // Stop bubble
126
126
  event.stopPropagation();
127
127
 
128
- // Clear any seed
129
- if (state.loadDataSeed != null) {
130
- clearTimeout(state.loadDataSeed);
131
- }
132
-
133
- // Delay 480 for loading data
134
- state.loadDataSeed = window.setTimeout(
135
- loadDataDirect,
136
- 480,
137
- event.currentTarget.value
138
- );
128
+ // Call with delay
129
+ delayed.call(undefined, event.currentTarget.value);
139
130
  };
140
131
 
141
132
  // Directly load data
142
133
  const loadDataDirect = (keyword?: string, id?: DataTypes.IdType) => {
143
- // Reset seed value
144
- state.loadDataSeed = undefined;
145
-
146
134
  // Reset options
147
135
  // setOptions([]);
148
136
 
@@ -176,6 +164,8 @@ export function Tiplist<T extends {} = IdLabelDto>(props: TiplistProps<T>) {
176
164
  });
177
165
  };
178
166
 
167
+ const delayed = useDelayedExecutor(loadDataDirect, 480);
168
+
179
169
  const setInputValue = (value: T | null) => {
180
170
  stateUpdate({ value });
181
171
 
@@ -207,12 +197,8 @@ export function Tiplist<T extends {} = IdLabelDto>(props: TiplistProps<T>) {
207
197
 
208
198
  React.useEffect(() => {
209
199
  return () => {
210
- // Clear any seed
211
- if (state.loadDataSeed != null) {
212
- clearTimeout(state.loadDataSeed);
213
- }
214
-
215
200
  isMounted.current = false;
201
+ delayed.clear();
216
202
  };
217
203
  }, []);
218
204
 
@@ -1,5 +1,6 @@
1
1
  import { ClickAwayListener, Tooltip, TooltipProps } from '@mui/material';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from '../uses/useDelayedExecutor';
3
4
 
4
5
  /**
5
6
  * Tooltip with click visibility props
@@ -36,32 +37,22 @@ export function TooltipClick(props: TooltipClickProps) {
36
37
  // State
37
38
  const [localTitle, setTitle] = React.useState(title);
38
39
  const [open, setOpen] = React.useState(false);
39
- const leaveSeed = React.useRef(0);
40
+
41
+ const delayed =
42
+ leaveDelay > 0
43
+ ? useDelayedExecutor(() => setOpen(false), leaveDelay)
44
+ : undefined;
40
45
 
41
46
  // Callback for open the tooltip
42
47
  const openTooltip = (newTitle?: string) => {
43
48
  setOpen(true);
44
49
  if (newTitle) setTitle(newTitle);
45
-
46
- if (leaveDelay > 0) {
47
- clearLeaveSeed();
48
- leaveSeed.current = window.setTimeout(
49
- () => setOpen(false),
50
- leaveDelay
51
- );
52
- }
53
- };
54
-
55
- const clearLeaveSeed = () => {
56
- if (leaveSeed.current > 0) {
57
- window.clearTimeout(leaveSeed.current);
58
- leaveSeed.current = 0;
59
- }
50
+ delayed?.call();
60
51
  };
61
52
 
62
53
  React.useEffect(() => {
63
54
  return () => {
64
- clearLeaveSeed();
55
+ delayed?.clear();
65
56
  };
66
57
  }, []);
67
58
 
@@ -0,0 +1,15 @@
1
+ import { DelayedExecutorType, ExtendUtils } from '@etsoo/shared';
2
+ import React from 'react';
3
+
4
+ /**
5
+ * Create delayed executor
6
+ */
7
+ export function useDelayedExecutor<P extends any[]>(
8
+ func: (...args: P) => void,
9
+ delayMiliseconcs: number
10
+ ) {
11
+ const ref = React.useRef<DelayedExecutorType<P>>();
12
+ if (ref.current == null)
13
+ ref.current = ExtendUtils.delayedExecutor(func, delayMiliseconcs);
14
+ return ref.current;
15
+ }
@@ -1,10 +1,10 @@
1
1
  import { DomUtils } from '@etsoo/shared';
2
2
  import React from 'react';
3
+ import { useDelayedExecutor } from './useDelayedExecutor';
3
4
 
4
5
  interface states {
5
6
  count: number;
6
7
  indices: number[];
7
- seed?: number;
8
8
  }
9
9
 
10
10
  /**
@@ -29,6 +29,45 @@ export function useDimensions(
29
29
  indices: []
30
30
  });
31
31
 
32
+ // Dimentions
33
+ const dimensions =
34
+ React.useRef<[React.RefCallback<Element>, Element?, DOMRect?][]>();
35
+ if (dimensions.current == null) {
36
+ // Init
37
+ const init: [React.RefCallback<Element>, Element?, DOMRect?][] = [];
38
+ for (let e = 0; e < elements; e++) {
39
+ init.push(
40
+ ((index): [React.RefCallback<Element>] => {
41
+ return [
42
+ (instance) => {
43
+ if (instance != null) {
44
+ // Current element
45
+ const currentElement = init[index][1];
46
+
47
+ if (currentElement != null) {
48
+ // Same target, return
49
+ if (currentElement == instance) return;
50
+
51
+ // Cancel observation
52
+ resizeObserver.unobserve(currentElement);
53
+ }
54
+
55
+ // Update element
56
+ init[index][1] = instance;
57
+
58
+ // Start observe
59
+ resizeObserver.observe(instance);
60
+ }
61
+ }
62
+ ];
63
+ })(e)
64
+ );
65
+ }
66
+ dimensions.current = init;
67
+ }
68
+
69
+ const delayed = useDelayedExecutor(setState, miliseconds);
70
+
32
71
  // Observer
33
72
  const resizeObserver = new ResizeObserver((entries) => {
34
73
  // Update Rect
@@ -62,21 +101,9 @@ export function useDimensions(
62
101
 
63
102
  // Update state
64
103
  if (indices.length > 0) {
65
- const update = { count: state.count + 1, indices, seed: undefined };
66
- if (miliseconds > 0) {
67
- if (state.seed != null) {
68
- clearTimeout(state.seed);
69
- }
70
- state.seed = window.setTimeout(
71
- (localUpdate: states) => {
72
- setState(localUpdate);
73
- },
74
- miliseconds,
75
- update
76
- );
77
- } else {
78
- setState(update);
79
- }
104
+ // Count only for unique update
105
+ const update = { count: state.count + 1, indices };
106
+ delayed.call(undefined, update);
80
107
  }
81
108
  });
82
109
 
@@ -111,21 +138,15 @@ export function useDimensions(
111
138
  );
112
139
  }
113
140
 
114
- // Dimensions
115
- const [dimensions] = React.useState(init);
116
-
117
141
  // Layout ready
118
142
  React.useEffect(() => {
119
143
  return () => {
120
144
  // Clear the observer
121
145
  resizeObserver.disconnect();
122
-
123
- if (state.seed != null) {
124
- clearTimeout(state.seed);
125
- }
146
+ delayed.clear();
126
147
  };
127
148
  }, []);
128
149
 
129
150
  // Return
130
- return { dimensions, state };
151
+ return { dimensions: dimensions.current, state };
131
152
  }
@@ -1,3 +1,4 @@
1
+ import { ExtendUtils } from '@etsoo/shared';
1
2
  import React from 'react';
2
3
 
3
4
  /**
@@ -5,31 +6,24 @@ import React from 'react';
5
6
  * @param action Action function
6
7
  * @param milliseconds Interval of milliseconds
7
8
  */
8
- export const useTimeout = (action: Function, milliseconds: number) => {
9
- // Time out seed
10
- let seed = 0;
11
-
12
- // Cancel function
13
- const cancel = () => {
14
- if (seed > 0) {
15
- clearTimeout(seed);
16
- seed = 0;
17
- }
18
- };
9
+ export const useTimeout = (
10
+ action: (...args: any[]) => void,
11
+ milliseconds: number
12
+ ) => {
13
+ // Delayed
14
+ const d = ExtendUtils.delayedExecutor(action, milliseconds);
19
15
 
20
16
  // Merge into the life cycle
21
17
  React.useEffect(() => {
22
- seed = window.setTimeout(() => {
23
- action.call(null);
24
- }, milliseconds);
18
+ d.call();
25
19
 
26
20
  return () => {
27
- cancel();
21
+ d.clear();
28
22
  };
29
23
  }, []);
30
24
 
31
25
  // Return cancel method
32
26
  return {
33
- cancel
27
+ cancel: d.clear
34
28
  };
35
- };
29
+ };