@etsoo/materialui 1.4.90 → 1.4.91

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.
@@ -112,6 +112,7 @@ export * from "./TiplistPro";
112
112
  export * from "./TagList";
113
113
  export * from "./TagListPro";
114
114
  export * from "./TwoFieldInput";
115
+ export * from "./useCurrentBreakpoint";
115
116
  export * from "./TooltipClick";
116
117
  export * from "./UserAvatar";
117
118
  export * from "./UserAvatarEditor";
package/lib/cjs/index.js CHANGED
@@ -128,6 +128,7 @@ __exportStar(require("./TiplistPro"), exports);
128
128
  __exportStar(require("./TagList"), exports);
129
129
  __exportStar(require("./TagListPro"), exports);
130
130
  __exportStar(require("./TwoFieldInput"), exports);
131
+ __exportStar(require("./useCurrentBreakpoint"), exports);
131
132
  __exportStar(require("./TooltipClick"), exports);
132
133
  __exportStar(require("./UserAvatar"), exports);
133
134
  __exportStar(require("./UserAvatarEditor"), exports);
@@ -1,9 +1,25 @@
1
1
  import { GridColumnRenderProps, GridDataType } from "@etsoo/react";
2
2
  import { DataTypes } from "@etsoo/shared";
3
- import { Grid2Props } from "@mui/material";
3
+ import { Breakpoint, Grid2Props } from "@mui/material";
4
4
  import React from "react";
5
5
  import { CommonPageProps } from "./CommonPage";
6
6
  import type { OperationMessageHandlerAll } from "../messages/OperationMessageHandler";
7
+ /**
8
+ * View page item size
9
+ */
10
+ export type ViewPageItemSize = Record<Breakpoint, number | undefined>;
11
+ /**
12
+ * View page grid item size
13
+ */
14
+ export declare namespace ViewPageSize {
15
+ const medium: ViewPageItemSize;
16
+ const line: ViewPageItemSize;
17
+ const small: ViewPageItemSize;
18
+ const smallLine: ViewPageItemSize;
19
+ function matchSize(size: ViewPageItemSize): {
20
+ [k: string]: number | undefined;
21
+ };
22
+ }
7
23
  /**
8
24
  * View page grid item properties
9
25
  */
@@ -21,7 +37,7 @@ export declare function ViewPageGridItem(props: ViewPageGridItemProps): import("
21
37
  /**
22
38
  * View page row width type
23
39
  */
24
- export type ViewPageRowType = boolean | "default" | "small" | "medium" | object;
40
+ export type ViewPageRowType = boolean | "default" | "small" | "medium" | ViewPageItemSize;
25
41
  /**
26
42
  * View page display field
27
43
  */
@@ -51,7 +67,7 @@ type ViewPageFieldTypeNarrow<T extends object> = (string & keyof T) | [string &
51
67
  /**
52
68
  * View page field type
53
69
  */
54
- export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode);
70
+ export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode | [React.ReactNode, ViewPageItemSize]);
55
71
  /**
56
72
  * View page props
57
73
  */
@@ -95,6 +111,26 @@ export interface ViewPageProps<T extends DataTypes.StringRecord> extends Omit<Co
95
111
  id: number;
96
112
  types: string[];
97
113
  };
114
+ /**
115
+ * Title bar
116
+ * @param data Data to render
117
+ * @returns
118
+ */
119
+ titleBar?: (data: T) => React.ReactNode;
120
+ /**
121
+ * Left container
122
+ */
123
+ leftContainer?: (data: T) => React.ReactNode;
124
+ /**
125
+ * Left container height in lines
126
+ */
127
+ leftContainerLines?: number;
128
+ /**
129
+ * Left container properties
130
+ */
131
+ leftContainerProps?: Omit<Grid2Props, "size"> & {
132
+ size?: ViewPageItemSize;
133
+ };
98
134
  }
99
135
  /**
100
136
  * View page
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ViewPageSize = void 0;
6
7
  exports.ViewPageGridItem = ViewPageGridItem;
7
8
  exports.ViewPage = ViewPage;
8
9
  const react_1 = require("react");
@@ -19,6 +20,43 @@ const CommonPage_1 = require("./CommonPage");
19
20
  const MessageUtils_1 = require("../messages/MessageUtils");
20
21
  const OperationMessageContainer_1 = require("../messages/OperationMessageContainer");
21
22
  const ReactApp_1 = require("../app/ReactApp");
23
+ const useCurrentBreakpoint_1 = require("../useCurrentBreakpoint");
24
+ const breakpoints = ["xs", "sm", "md", "lg", "xl"];
25
+ /**
26
+ * View page grid item size
27
+ */
28
+ var ViewPageSize;
29
+ (function (ViewPageSize) {
30
+ ViewPageSize.medium = {
31
+ xs: 12,
32
+ sm: 12,
33
+ md: 6,
34
+ lg: 4,
35
+ xl: 3
36
+ };
37
+ ViewPageSize.line = {
38
+ xs: 12,
39
+ sm: 12,
40
+ md: 12,
41
+ lg: 12,
42
+ xl: 12
43
+ };
44
+ ViewPageSize.small = { xs: 6, sm: 6, md: 4, lg: 3, xl: 2 };
45
+ ViewPageSize.smallLine = {
46
+ xs: 12,
47
+ sm: 6,
48
+ md: 4,
49
+ lg: 3,
50
+ xl: 2
51
+ };
52
+ function matchSize(size) {
53
+ return Object.fromEntries(Object.entries(size).map(([key, value]) => [
54
+ key,
55
+ value == null ? undefined : value === 12 ? 12 : 12 - value
56
+ ]));
57
+ }
58
+ ViewPageSize.matchSize = matchSize;
59
+ })(ViewPageSize || (exports.ViewPageSize = ViewPageSize = {}));
22
60
  /**
23
61
  * View page grid item
24
62
  * @param props Props
@@ -51,33 +89,32 @@ function getResp(singleRow) {
51
89
  const size = typeof singleRow === "object"
52
90
  ? singleRow
53
91
  : singleRow === "medium"
54
- ? { xs: 12, sm: 12, md: 6, lg: 4, xl: 3 }
92
+ ? ViewPageSize.medium
55
93
  : singleRow === true
56
- ? { xs: 12, sm: 12, md: 12, lg: 12, xl: 12 }
57
- : {
58
- xs: singleRow === false ? 12 : 6,
59
- sm: 6,
60
- md: 4,
61
- lg: 3,
62
- xl: 2
63
- };
64
- return { size };
94
+ ? ViewPageSize.line
95
+ : singleRow === false
96
+ ? ViewPageSize.smallLine
97
+ : ViewPageSize.small;
98
+ return size;
65
99
  }
66
100
  function getItemField(app, field, data) {
67
101
  // Item data and label
68
- let itemData, itemLabel, gridProps = {};
102
+ let itemData, itemLabel, gridProps = {}, size;
69
103
  if (Array.isArray(field)) {
70
104
  const [fieldData, fieldType, renderProps, singleRow = "small"] = field;
71
105
  itemData = (0, GridDataFormat_1.GridDataFormat)(data[fieldData], fieldType, renderProps);
72
106
  itemLabel = app.get(fieldData) ?? fieldData;
73
- gridProps = { ...getResp(singleRow) };
107
+ size = getResp(singleRow);
108
+ gridProps = { size };
74
109
  }
75
110
  else if (typeof field === "object") {
76
111
  // Destruct
77
112
  const { data: fieldData, dataType, label: fieldLabel, renderProps, singleRow = "default", ...rest } = field;
113
+ // Size
114
+ size = getResp(singleRow);
78
115
  gridProps = {
79
116
  ...rest,
80
- ...getResp(singleRow)
117
+ size
81
118
  };
82
119
  // Field data
83
120
  if (typeof fieldData === "function")
@@ -102,8 +139,22 @@ function getItemField(app, field, data) {
102
139
  // Single field format
103
140
  itemData = formatItemData(app, data[field]);
104
141
  itemLabel = app.get(field) ?? field;
142
+ size = ViewPageSize.small;
143
+ gridProps = { size };
105
144
  }
106
- return [itemData, itemLabel, gridProps];
145
+ return [itemData, itemLabel, gridProps, size];
146
+ }
147
+ function getItemSize(bp, size) {
148
+ const v = size[bp];
149
+ if (v != null)
150
+ return v;
151
+ const index = breakpoints.indexOf(bp);
152
+ for (let i = index; i >= 0; i--) {
153
+ const v = size[breakpoints[i]];
154
+ if (v != null)
155
+ return v;
156
+ }
157
+ return 12;
107
158
  }
108
159
  /**
109
160
  * View page
@@ -113,19 +164,80 @@ function ViewPage(props) {
113
164
  // Global app
114
165
  const app = (0, ReactApp_1.useRequiredAppContext)();
115
166
  // Destruct
116
- const { actions, children, fields, loadData, paddings = MUGlobal_1.MUGlobal.pagePaddings, spacing = MUGlobal_1.MUGlobal.half(MUGlobal_1.MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, ...rest } = props;
167
+ const { actions, children, fields, loadData, paddings = MUGlobal_1.MUGlobal.pagePaddings, spacing = MUGlobal_1.MUGlobal.half(MUGlobal_1.MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, titleBar, leftContainer, leftContainerLines = 3, leftContainerProps = {}, ...rest } = props;
168
+ // Current breakpoint
169
+ const bp = (0, useCurrentBreakpoint_1.useCurrentBreakpoint)();
117
170
  // Data
118
171
  const [data, setData] = react_3.default.useState();
119
172
  // Labels
120
173
  const labels = Labels_1.Labels.CommonPage;
121
174
  // Container
122
175
  const pullContainer = "#page-container";
176
+ // Left container
177
+ const { size = ViewPageSize.smallLine, ...leftContainerPropsRest } = leftContainerProps;
123
178
  // Load data
124
179
  const refresh = react_3.default.useCallback(async () => {
125
180
  const result = await loadData();
126
181
  // When failed or no data returned, show the loading bar
127
182
  setData(result);
128
183
  }, [loadData]);
184
+ // Create fields
185
+ const fieldIndexRef = react_3.default.useRef(0);
186
+ const createFields = react_3.default.useCallback((data, maxItems = 0) => {
187
+ let validItems = 0;
188
+ const items = [];
189
+ let i = fieldIndexRef.current;
190
+ for (; i < fields.length; i++) {
191
+ const field = fields[i];
192
+ let oneSize;
193
+ let oneItem;
194
+ if (typeof field === "function") {
195
+ // Most flexible way, do whatever you want
196
+ const createdResult = field(data, refresh);
197
+ if (createdResult == null || createdResult === "")
198
+ continue;
199
+ if (Array.isArray(createdResult)) {
200
+ const [created, size] = createdResult;
201
+ oneSize = size;
202
+ oneItem = created;
203
+ }
204
+ else {
205
+ oneSize = ViewPageSize.line;
206
+ oneItem = createdResult;
207
+ }
208
+ }
209
+ else {
210
+ const [itemData, itemLabel, gridProps, size] = getItemField(app, field, data);
211
+ // Some callback function may return '' instead of undefined
212
+ if (itemData == null || itemData === "")
213
+ continue;
214
+ oneSize = size;
215
+ oneItem = ((0, react_1.createElement)(ViewPageGridItem, { ...gridProps, key: i, data: itemData, label: itemLabel }));
216
+ }
217
+ // Max lines
218
+ if (maxItems > 0) {
219
+ const itemSize = getItemSize(bp, oneSize);
220
+ if (maxItems < validItems + itemSize) {
221
+ fieldIndexRef.current = i;
222
+ break;
223
+ }
224
+ else {
225
+ items.push(oneItem);
226
+ validItems += itemSize;
227
+ }
228
+ }
229
+ else {
230
+ items.push(oneItem);
231
+ }
232
+ }
233
+ if (maxItems === 0) {
234
+ fieldIndexRef.current = 0;
235
+ }
236
+ else {
237
+ fieldIndexRef.current = i;
238
+ }
239
+ return items;
240
+ }, [app, refresh, fields, data, bp]);
129
241
  react_3.default.useEffect(() => {
130
242
  const refreshHandler = async () => {
131
243
  await refresh();
@@ -141,23 +253,11 @@ function ViewPage(props) {
141
253
  refresh,
142
254
  operationMessageHandler.id
143
255
  ]
144
- : operationMessageHandler })), (0, jsx_runtime_1.jsx)(material_1.Grid2, { container: true, justifyContent: "left", spacing: spacing, className: "ET-ViewPage", ref: gridRef, sx: {
256
+ : operationMessageHandler })), titleBar && titleBar(data), (0, jsx_runtime_1.jsxs)(material_1.Grid2, { container: true, justifyContent: "left", className: "ET-ViewPage", ref: gridRef, spacing: spacing, sx: {
145
257
  ".MuiTypography-subtitle2": {
146
258
  fontWeight: "bold"
147
259
  }
148
- }, children: fields.map((field, index) => {
149
- // Get data
150
- if (typeof field === "function") {
151
- // Most flexible way, do whatever you want
152
- return field(data, refresh);
153
- }
154
- const [itemData, itemLabel, gridProps] = getItemField(app, field, data);
155
- // Some callback function may return '' instead of undefined
156
- if (itemData == null || itemData === "")
157
- return undefined;
158
- // Layout
159
- return ((0, react_1.createElement)(ViewPageGridItem, { ...gridProps, key: index, data: itemData, label: itemLabel }));
160
- }) }), actions !== null && ((0, jsx_runtime_1.jsx)(material_1.Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && shared_1.Utils.getResult(actions, data, refresh) })), shared_1.Utils.getResult(children, data, refresh), pullToRefresh && ((0, jsx_runtime_1.jsx)(PullToRefreshUI_1.PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
260
+ }, children: [leftContainer && ((0, jsx_runtime_1.jsxs)(react_3.default.Fragment, { children: [(0, jsx_runtime_1.jsx)(material_1.Grid2, { container: true, className: "ET-ViewPage-LeftContainer", spacing: spacing, size: size, ...leftContainerPropsRest, children: leftContainer(data) }), (0, jsx_runtime_1.jsx)(material_1.Grid2, { container: true, className: "ET-ViewPage-LeftOthers", spacing: spacing, size: ViewPageSize.matchSize(size), children: createFields(data, leftContainerLines * (12 - getItemSize(bp, size))) })] })), createFields(data)] }), actions !== null && ((0, jsx_runtime_1.jsx)(material_1.Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && shared_1.Utils.getResult(actions, data, refresh) })), shared_1.Utils.getResult(children, data, refresh), pullToRefresh && ((0, jsx_runtime_1.jsx)(PullToRefreshUI_1.PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
161
261
  const container = document.querySelector(pullContainer);
162
262
  return !container?.scrollTop;
163
263
  } })), (0, jsx_runtime_1.jsx)(react_2.ScrollRestoration, {})] })) }));
@@ -0,0 +1,6 @@
1
+ import { Breakpoint } from "@mui/material";
2
+ /**
3
+ * Hook to get the current breakpoint
4
+ * @returns The current breakpoint
5
+ */
6
+ export declare function useCurrentBreakpoint(): Breakpoint;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCurrentBreakpoint = useCurrentBreakpoint;
4
+ const material_1 = require("@mui/material");
5
+ /**
6
+ * Hook to get the current breakpoint
7
+ * @returns The current breakpoint
8
+ */
9
+ function useCurrentBreakpoint() {
10
+ const theme = (0, material_1.useTheme)();
11
+ const items = [
12
+ (0, material_1.useMediaQuery)(theme.breakpoints.down("xs")) ? "xs" : null,
13
+ (0, material_1.useMediaQuery)(theme.breakpoints.between("xs", "sm")) ? "sm" : null,
14
+ (0, material_1.useMediaQuery)(theme.breakpoints.between("sm", "md")) ? "md" : null,
15
+ (0, material_1.useMediaQuery)(theme.breakpoints.between("md", "lg")) ? "lg" : null,
16
+ (0, material_1.useMediaQuery)(theme.breakpoints.up("lg")) ? "xl" : null
17
+ ];
18
+ return items.find((item) => item != null) ?? "lg";
19
+ }
@@ -112,6 +112,7 @@ export * from "./TiplistPro";
112
112
  export * from "./TagList";
113
113
  export * from "./TagListPro";
114
114
  export * from "./TwoFieldInput";
115
+ export * from "./useCurrentBreakpoint";
115
116
  export * from "./TooltipClick";
116
117
  export * from "./UserAvatar";
117
118
  export * from "./UserAvatarEditor";
package/lib/mjs/index.js CHANGED
@@ -112,6 +112,7 @@ export * from "./TiplistPro";
112
112
  export * from "./TagList";
113
113
  export * from "./TagListPro";
114
114
  export * from "./TwoFieldInput";
115
+ export * from "./useCurrentBreakpoint";
115
116
  export * from "./TooltipClick";
116
117
  export * from "./UserAvatar";
117
118
  export * from "./UserAvatarEditor";
@@ -1,9 +1,25 @@
1
1
  import { GridColumnRenderProps, GridDataType } from "@etsoo/react";
2
2
  import { DataTypes } from "@etsoo/shared";
3
- import { Grid2Props } from "@mui/material";
3
+ import { Breakpoint, Grid2Props } from "@mui/material";
4
4
  import React from "react";
5
5
  import { CommonPageProps } from "./CommonPage";
6
6
  import type { OperationMessageHandlerAll } from "../messages/OperationMessageHandler";
7
+ /**
8
+ * View page item size
9
+ */
10
+ export type ViewPageItemSize = Record<Breakpoint, number | undefined>;
11
+ /**
12
+ * View page grid item size
13
+ */
14
+ export declare namespace ViewPageSize {
15
+ const medium: ViewPageItemSize;
16
+ const line: ViewPageItemSize;
17
+ const small: ViewPageItemSize;
18
+ const smallLine: ViewPageItemSize;
19
+ function matchSize(size: ViewPageItemSize): {
20
+ [k: string]: number | undefined;
21
+ };
22
+ }
7
23
  /**
8
24
  * View page grid item properties
9
25
  */
@@ -21,7 +37,7 @@ export declare function ViewPageGridItem(props: ViewPageGridItemProps): import("
21
37
  /**
22
38
  * View page row width type
23
39
  */
24
- export type ViewPageRowType = boolean | "default" | "small" | "medium" | object;
40
+ export type ViewPageRowType = boolean | "default" | "small" | "medium" | ViewPageItemSize;
25
41
  /**
26
42
  * View page display field
27
43
  */
@@ -51,7 +67,7 @@ type ViewPageFieldTypeNarrow<T extends object> = (string & keyof T) | [string &
51
67
  /**
52
68
  * View page field type
53
69
  */
54
- export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode);
70
+ export type ViewPageFieldType<T extends object> = ViewPageFieldTypeNarrow<T> | ((data: T, refresh: () => Promise<void>) => React.ReactNode | [React.ReactNode, ViewPageItemSize]);
55
71
  /**
56
72
  * View page props
57
73
  */
@@ -95,6 +111,26 @@ export interface ViewPageProps<T extends DataTypes.StringRecord> extends Omit<Co
95
111
  id: number;
96
112
  types: string[];
97
113
  };
114
+ /**
115
+ * Title bar
116
+ * @param data Data to render
117
+ * @returns
118
+ */
119
+ titleBar?: (data: T) => React.ReactNode;
120
+ /**
121
+ * Left container
122
+ */
123
+ leftContainer?: (data: T) => React.ReactNode;
124
+ /**
125
+ * Left container height in lines
126
+ */
127
+ leftContainerLines?: number;
128
+ /**
129
+ * Left container properties
130
+ */
131
+ leftContainerProps?: Omit<Grid2Props, "size"> & {
132
+ size?: ViewPageItemSize;
133
+ };
98
134
  }
99
135
  /**
100
136
  * View page
@@ -12,6 +12,43 @@ import { CommonPage } from "./CommonPage";
12
12
  import { MessageUtils } from "../messages/MessageUtils";
13
13
  import { OperationMessageContainer } from "../messages/OperationMessageContainer";
14
14
  import { useRequiredAppContext } from "../app/ReactApp";
15
+ import { useCurrentBreakpoint } from "../useCurrentBreakpoint";
16
+ const breakpoints = ["xs", "sm", "md", "lg", "xl"];
17
+ /**
18
+ * View page grid item size
19
+ */
20
+ export var ViewPageSize;
21
+ (function (ViewPageSize) {
22
+ ViewPageSize.medium = {
23
+ xs: 12,
24
+ sm: 12,
25
+ md: 6,
26
+ lg: 4,
27
+ xl: 3
28
+ };
29
+ ViewPageSize.line = {
30
+ xs: 12,
31
+ sm: 12,
32
+ md: 12,
33
+ lg: 12,
34
+ xl: 12
35
+ };
36
+ ViewPageSize.small = { xs: 6, sm: 6, md: 4, lg: 3, xl: 2 };
37
+ ViewPageSize.smallLine = {
38
+ xs: 12,
39
+ sm: 6,
40
+ md: 4,
41
+ lg: 3,
42
+ xl: 2
43
+ };
44
+ function matchSize(size) {
45
+ return Object.fromEntries(Object.entries(size).map(([key, value]) => [
46
+ key,
47
+ value == null ? undefined : value === 12 ? 12 : 12 - value
48
+ ]));
49
+ }
50
+ ViewPageSize.matchSize = matchSize;
51
+ })(ViewPageSize || (ViewPageSize = {}));
15
52
  /**
16
53
  * View page grid item
17
54
  * @param props Props
@@ -44,33 +81,32 @@ function getResp(singleRow) {
44
81
  const size = typeof singleRow === "object"
45
82
  ? singleRow
46
83
  : singleRow === "medium"
47
- ? { xs: 12, sm: 12, md: 6, lg: 4, xl: 3 }
84
+ ? ViewPageSize.medium
48
85
  : singleRow === true
49
- ? { xs: 12, sm: 12, md: 12, lg: 12, xl: 12 }
50
- : {
51
- xs: singleRow === false ? 12 : 6,
52
- sm: 6,
53
- md: 4,
54
- lg: 3,
55
- xl: 2
56
- };
57
- return { size };
86
+ ? ViewPageSize.line
87
+ : singleRow === false
88
+ ? ViewPageSize.smallLine
89
+ : ViewPageSize.small;
90
+ return size;
58
91
  }
59
92
  function getItemField(app, field, data) {
60
93
  // Item data and label
61
- let itemData, itemLabel, gridProps = {};
94
+ let itemData, itemLabel, gridProps = {}, size;
62
95
  if (Array.isArray(field)) {
63
96
  const [fieldData, fieldType, renderProps, singleRow = "small"] = field;
64
97
  itemData = GridDataFormat(data[fieldData], fieldType, renderProps);
65
98
  itemLabel = app.get(fieldData) ?? fieldData;
66
- gridProps = { ...getResp(singleRow) };
99
+ size = getResp(singleRow);
100
+ gridProps = { size };
67
101
  }
68
102
  else if (typeof field === "object") {
69
103
  // Destruct
70
104
  const { data: fieldData, dataType, label: fieldLabel, renderProps, singleRow = "default", ...rest } = field;
105
+ // Size
106
+ size = getResp(singleRow);
71
107
  gridProps = {
72
108
  ...rest,
73
- ...getResp(singleRow)
109
+ size
74
110
  };
75
111
  // Field data
76
112
  if (typeof fieldData === "function")
@@ -95,8 +131,22 @@ function getItemField(app, field, data) {
95
131
  // Single field format
96
132
  itemData = formatItemData(app, data[field]);
97
133
  itemLabel = app.get(field) ?? field;
134
+ size = ViewPageSize.small;
135
+ gridProps = { size };
98
136
  }
99
- return [itemData, itemLabel, gridProps];
137
+ return [itemData, itemLabel, gridProps, size];
138
+ }
139
+ function getItemSize(bp, size) {
140
+ const v = size[bp];
141
+ if (v != null)
142
+ return v;
143
+ const index = breakpoints.indexOf(bp);
144
+ for (let i = index; i >= 0; i--) {
145
+ const v = size[breakpoints[i]];
146
+ if (v != null)
147
+ return v;
148
+ }
149
+ return 12;
100
150
  }
101
151
  /**
102
152
  * View page
@@ -106,19 +156,80 @@ export function ViewPage(props) {
106
156
  // Global app
107
157
  const app = useRequiredAppContext();
108
158
  // Destruct
109
- const { actions, children, fields, loadData, paddings = MUGlobal.pagePaddings, spacing = MUGlobal.half(MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, ...rest } = props;
159
+ const { actions, children, fields, loadData, paddings = MUGlobal.pagePaddings, spacing = MUGlobal.half(MUGlobal.pagePaddings), supportRefresh = true, fabColumnDirection = true, fabTop = true, supportBack = true, pullToRefresh = true, gridRef, operationMessageHandler, titleBar, leftContainer, leftContainerLines = 3, leftContainerProps = {}, ...rest } = props;
160
+ // Current breakpoint
161
+ const bp = useCurrentBreakpoint();
110
162
  // Data
111
163
  const [data, setData] = React.useState();
112
164
  // Labels
113
165
  const labels = Labels.CommonPage;
114
166
  // Container
115
167
  const pullContainer = "#page-container";
168
+ // Left container
169
+ const { size = ViewPageSize.smallLine, ...leftContainerPropsRest } = leftContainerProps;
116
170
  // Load data
117
171
  const refresh = React.useCallback(async () => {
118
172
  const result = await loadData();
119
173
  // When failed or no data returned, show the loading bar
120
174
  setData(result);
121
175
  }, [loadData]);
176
+ // Create fields
177
+ const fieldIndexRef = React.useRef(0);
178
+ const createFields = React.useCallback((data, maxItems = 0) => {
179
+ let validItems = 0;
180
+ const items = [];
181
+ let i = fieldIndexRef.current;
182
+ for (; i < fields.length; i++) {
183
+ const field = fields[i];
184
+ let oneSize;
185
+ let oneItem;
186
+ if (typeof field === "function") {
187
+ // Most flexible way, do whatever you want
188
+ const createdResult = field(data, refresh);
189
+ if (createdResult == null || createdResult === "")
190
+ continue;
191
+ if (Array.isArray(createdResult)) {
192
+ const [created, size] = createdResult;
193
+ oneSize = size;
194
+ oneItem = created;
195
+ }
196
+ else {
197
+ oneSize = ViewPageSize.line;
198
+ oneItem = createdResult;
199
+ }
200
+ }
201
+ else {
202
+ const [itemData, itemLabel, gridProps, size] = getItemField(app, field, data);
203
+ // Some callback function may return '' instead of undefined
204
+ if (itemData == null || itemData === "")
205
+ continue;
206
+ oneSize = size;
207
+ oneItem = (_createElement(ViewPageGridItem, { ...gridProps, key: i, data: itemData, label: itemLabel }));
208
+ }
209
+ // Max lines
210
+ if (maxItems > 0) {
211
+ const itemSize = getItemSize(bp, oneSize);
212
+ if (maxItems < validItems + itemSize) {
213
+ fieldIndexRef.current = i;
214
+ break;
215
+ }
216
+ else {
217
+ items.push(oneItem);
218
+ validItems += itemSize;
219
+ }
220
+ }
221
+ else {
222
+ items.push(oneItem);
223
+ }
224
+ }
225
+ if (maxItems === 0) {
226
+ fieldIndexRef.current = 0;
227
+ }
228
+ else {
229
+ fieldIndexRef.current = i;
230
+ }
231
+ return items;
232
+ }, [app, refresh, fields, data, bp]);
122
233
  React.useEffect(() => {
123
234
  const refreshHandler = async () => {
124
235
  await refresh();
@@ -134,23 +245,11 @@ export function ViewPage(props) {
134
245
  refresh,
135
246
  operationMessageHandler.id
136
247
  ]
137
- : operationMessageHandler })), _jsx(Grid2, { container: true, justifyContent: "left", spacing: spacing, className: "ET-ViewPage", ref: gridRef, sx: {
248
+ : operationMessageHandler })), titleBar && titleBar(data), _jsxs(Grid2, { container: true, justifyContent: "left", className: "ET-ViewPage", ref: gridRef, spacing: spacing, sx: {
138
249
  ".MuiTypography-subtitle2": {
139
250
  fontWeight: "bold"
140
251
  }
141
- }, children: fields.map((field, index) => {
142
- // Get data
143
- if (typeof field === "function") {
144
- // Most flexible way, do whatever you want
145
- return field(data, refresh);
146
- }
147
- const [itemData, itemLabel, gridProps] = getItemField(app, field, data);
148
- // Some callback function may return '' instead of undefined
149
- if (itemData == null || itemData === "")
150
- return undefined;
151
- // Layout
152
- return (_createElement(ViewPageGridItem, { ...gridProps, key: index, data: itemData, label: itemLabel }));
153
- }) }), actions !== null && (_jsx(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && Utils.getResult(actions, data, refresh) })), Utils.getResult(children, data, refresh), pullToRefresh && (_jsx(PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
252
+ }, children: [leftContainer && (_jsxs(React.Fragment, { children: [_jsx(Grid2, { container: true, className: "ET-ViewPage-LeftContainer", spacing: spacing, size: size, ...leftContainerPropsRest, children: leftContainer(data) }), _jsx(Grid2, { container: true, className: "ET-ViewPage-LeftOthers", spacing: spacing, size: ViewPageSize.matchSize(size), children: createFields(data, leftContainerLines * (12 - getItemSize(bp, size))) })] })), createFields(data)] }), actions !== null && (_jsx(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: actions == null ? undefined : paddings, paddingBottom: paddings, gap: paddings, children: actions != null && Utils.getResult(actions, data, refresh) })), Utils.getResult(children, data, refresh), pullToRefresh && (_jsx(PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
154
253
  const container = document.querySelector(pullContainer);
155
254
  return !container?.scrollTop;
156
255
  } })), _jsx(ScrollRestoration, {})] })) }));
@@ -0,0 +1,6 @@
1
+ import { Breakpoint } from "@mui/material";
2
+ /**
3
+ * Hook to get the current breakpoint
4
+ * @returns The current breakpoint
5
+ */
6
+ export declare function useCurrentBreakpoint(): Breakpoint;
@@ -0,0 +1,16 @@
1
+ import { useMediaQuery, useTheme } from "@mui/material";
2
+ /**
3
+ * Hook to get the current breakpoint
4
+ * @returns The current breakpoint
5
+ */
6
+ export function useCurrentBreakpoint() {
7
+ const theme = useTheme();
8
+ const items = [
9
+ useMediaQuery(theme.breakpoints.down("xs")) ? "xs" : null,
10
+ useMediaQuery(theme.breakpoints.between("xs", "sm")) ? "sm" : null,
11
+ useMediaQuery(theme.breakpoints.between("sm", "md")) ? "md" : null,
12
+ useMediaQuery(theme.breakpoints.between("md", "lg")) ? "lg" : null,
13
+ useMediaQuery(theme.breakpoints.up("lg")) ? "xl" : null
14
+ ];
15
+ return items.find((item) => item != null) ?? "lg";
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/materialui",
3
- "version": "1.4.90",
3
+ "version": "1.4.91",
4
4
  "description": "TypeScript Material-UI Implementation",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/mjs/index.js",
@@ -28,7 +28,6 @@ import {
28
28
  NotificationReactCallProps,
29
29
  UserAction,
30
30
  UserActionType,
31
- UserCalls,
32
31
  useRequiredContext,
33
32
  UserState
34
33
  } from "@etsoo/react";
package/src/index.ts CHANGED
@@ -117,6 +117,7 @@ export * from "./TiplistPro";
117
117
  export * from "./TagList";
118
118
  export * from "./TagListPro";
119
119
  export * from "./TwoFieldInput";
120
+ export * from "./useCurrentBreakpoint";
120
121
  export * from "./TooltipClick";
121
122
  export * from "./UserAvatar";
122
123
  export * from "./UserAvatarEditor";
@@ -5,6 +5,7 @@ import {
5
5
  } from "@etsoo/react";
6
6
  import { DataTypes, Utils } from "@etsoo/shared";
7
7
  import {
8
+ Breakpoint,
8
9
  Grid2,
9
10
  Grid2Props,
10
11
  LinearProgress,
@@ -22,6 +23,50 @@ import { MessageUtils } from "../messages/MessageUtils";
22
23
  import type { RefreshHandler } from "../messages/RefreshHandler";
23
24
  import { OperationMessageContainer } from "../messages/OperationMessageContainer";
24
25
  import { ReactAppType, useRequiredAppContext } from "../app/ReactApp";
26
+ import { useCurrentBreakpoint } from "../useCurrentBreakpoint";
27
+
28
+ /**
29
+ * View page item size
30
+ */
31
+ export type ViewPageItemSize = Record<Breakpoint, number | undefined>;
32
+
33
+ const breakpoints: Breakpoint[] = ["xs", "sm", "md", "lg", "xl"];
34
+
35
+ /**
36
+ * View page grid item size
37
+ */
38
+ export namespace ViewPageSize {
39
+ export const medium: ViewPageItemSize = {
40
+ xs: 12,
41
+ sm: 12,
42
+ md: 6,
43
+ lg: 4,
44
+ xl: 3
45
+ };
46
+ export const line: ViewPageItemSize = {
47
+ xs: 12,
48
+ sm: 12,
49
+ md: 12,
50
+ lg: 12,
51
+ xl: 12
52
+ };
53
+ export const small: ViewPageItemSize = { xs: 6, sm: 6, md: 4, lg: 3, xl: 2 };
54
+ export const smallLine: ViewPageItemSize = {
55
+ xs: 12,
56
+ sm: 6,
57
+ md: 4,
58
+ lg: 3,
59
+ xl: 2
60
+ };
61
+ export function matchSize(size: ViewPageItemSize) {
62
+ return Object.fromEntries(
63
+ Object.entries(size).map(([key, value]) => [
64
+ key,
65
+ value == null ? undefined : value === 12 ? 12 : 12 - value
66
+ ])
67
+ );
68
+ }
69
+ }
25
70
 
26
71
  /**
27
72
  * View page grid item properties
@@ -69,7 +114,12 @@ export function ViewPageGridItem(props: ViewPageGridItemProps) {
69
114
  /**
70
115
  * View page row width type
71
116
  */
72
- export type ViewPageRowType = boolean | "default" | "small" | "medium" | object;
117
+ export type ViewPageRowType =
118
+ | boolean
119
+ | "default"
120
+ | "small"
121
+ | "medium"
122
+ | ViewPageItemSize;
73
123
 
74
124
  /**
75
125
  * View page display field
@@ -111,7 +161,10 @@ type ViewPageFieldTypeNarrow<T extends object> =
111
161
  */
112
162
  export type ViewPageFieldType<T extends object> =
113
163
  | ViewPageFieldTypeNarrow<T>
114
- | ((data: T, refresh: () => Promise<void>) => React.ReactNode);
164
+ | ((
165
+ data: T,
166
+ refresh: () => Promise<void>
167
+ ) => React.ReactNode | [React.ReactNode, ViewPageItemSize]);
115
168
 
116
169
  /**
117
170
  * View page props
@@ -168,6 +221,28 @@ export interface ViewPageProps<T extends DataTypes.StringRecord>
168
221
  operationMessageHandler?:
169
222
  | OperationMessageHandlerAll
170
223
  | { id: number; types: string[] };
224
+
225
+ /**
226
+ * Title bar
227
+ * @param data Data to render
228
+ * @returns
229
+ */
230
+ titleBar?: (data: T) => React.ReactNode;
231
+
232
+ /**
233
+ * Left container
234
+ */
235
+ leftContainer?: (data: T) => React.ReactNode;
236
+
237
+ /**
238
+ * Left container height in lines
239
+ */
240
+ leftContainerLines?: number;
241
+
242
+ /**
243
+ * Left container properties
244
+ */
245
+ leftContainerProps?: Omit<Grid2Props, "size"> & { size?: ViewPageItemSize };
171
246
  }
172
247
 
173
248
  function formatItemData(
@@ -185,34 +260,32 @@ function getResp(singleRow: ViewPageRowType) {
185
260
  typeof singleRow === "object"
186
261
  ? singleRow
187
262
  : singleRow === "medium"
188
- ? { xs: 12, sm: 12, md: 6, lg: 4, xl: 3 }
263
+ ? ViewPageSize.medium
189
264
  : singleRow === true
190
- ? { xs: 12, sm: 12, md: 12, lg: 12, xl: 12 }
191
- : {
192
- xs: singleRow === false ? 12 : 6,
193
- sm: 6,
194
- md: 4,
195
- lg: 3,
196
- xl: 2
197
- };
198
- return { size };
265
+ ? ViewPageSize.line
266
+ : singleRow === false
267
+ ? ViewPageSize.smallLine
268
+ : ViewPageSize.small;
269
+ return size;
199
270
  }
200
271
 
201
272
  function getItemField<T extends object>(
202
273
  app: ReactAppType,
203
274
  field: ViewPageFieldTypeNarrow<T>,
204
275
  data: T
205
- ): [React.ReactNode, React.ReactNode, Grid2Props] {
276
+ ): [React.ReactNode, React.ReactNode, Grid2Props, ViewPageItemSize] {
206
277
  // Item data and label
207
278
  let itemData: React.ReactNode,
208
279
  itemLabel: React.ReactNode,
209
- gridProps: Grid2Props = {};
280
+ gridProps: Grid2Props = {},
281
+ size: ViewPageItemSize;
210
282
 
211
283
  if (Array.isArray(field)) {
212
284
  const [fieldData, fieldType, renderProps, singleRow = "small"] = field;
213
285
  itemData = GridDataFormat(data[fieldData], fieldType, renderProps);
214
286
  itemLabel = app.get<string>(fieldData) ?? fieldData;
215
- gridProps = { ...getResp(singleRow) };
287
+ size = getResp(singleRow);
288
+ gridProps = { size };
216
289
  } else if (typeof field === "object") {
217
290
  // Destruct
218
291
  const {
@@ -224,9 +297,12 @@ function getItemField<T extends object>(
224
297
  ...rest
225
298
  } = field;
226
299
 
300
+ // Size
301
+ size = getResp(singleRow);
302
+
227
303
  gridProps = {
228
304
  ...rest,
229
- ...getResp(singleRow)
305
+ size
230
306
  };
231
307
 
232
308
  // Field data
@@ -249,9 +325,24 @@ function getItemField<T extends object>(
249
325
  // Single field format
250
326
  itemData = formatItemData(app, data[field]);
251
327
  itemLabel = app.get<string>(field) ?? field;
328
+ size = ViewPageSize.small;
329
+ gridProps = { size };
330
+ }
331
+
332
+ return [itemData, itemLabel, gridProps, size];
333
+ }
334
+
335
+ function getItemSize(bp: Breakpoint, size: ViewPageItemSize) {
336
+ const v = size[bp];
337
+ if (v != null) return v;
338
+
339
+ const index = breakpoints.indexOf(bp);
340
+ for (let i = index; i >= 0; i--) {
341
+ const v = size[breakpoints[i]];
342
+ if (v != null) return v;
252
343
  }
253
344
 
254
- return [itemData, itemLabel, gridProps];
345
+ return 12;
255
346
  }
256
347
 
257
348
  /**
@@ -279,9 +370,16 @@ export function ViewPage<T extends DataTypes.StringRecord>(
279
370
  pullToRefresh = true,
280
371
  gridRef,
281
372
  operationMessageHandler,
373
+ titleBar,
374
+ leftContainer,
375
+ leftContainerLines = 3,
376
+ leftContainerProps = {},
282
377
  ...rest
283
378
  } = props;
284
379
 
380
+ // Current breakpoint
381
+ const bp = useCurrentBreakpoint();
382
+
285
383
  // Data
286
384
  const [data, setData] = React.useState<T>();
287
385
 
@@ -291,6 +389,10 @@ export function ViewPage<T extends DataTypes.StringRecord>(
291
389
  // Container
292
390
  const pullContainer = "#page-container";
293
391
 
392
+ // Left container
393
+ const { size = ViewPageSize.smallLine, ...leftContainerPropsRest } =
394
+ leftContainerProps;
395
+
294
396
  // Load data
295
397
  const refresh = React.useCallback(async () => {
296
398
  const result = await loadData();
@@ -298,6 +400,76 @@ export function ViewPage<T extends DataTypes.StringRecord>(
298
400
  setData(result);
299
401
  }, [loadData]);
300
402
 
403
+ // Create fields
404
+ const fieldIndexRef = React.useRef(0);
405
+ const createFields = React.useCallback(
406
+ (data: T, maxItems: number = 0) => {
407
+ let validItems = 0;
408
+ const items: React.ReactNode[] = [];
409
+ let i: number = fieldIndexRef.current;
410
+ for (; i < fields.length; i++) {
411
+ const field = fields[i];
412
+ let oneSize: ViewPageItemSize;
413
+ let oneItem: React.ReactNode;
414
+ if (typeof field === "function") {
415
+ // Most flexible way, do whatever you want
416
+ const createdResult = field(data, refresh);
417
+ if (createdResult == null || createdResult === "") continue;
418
+ if (Array.isArray(createdResult)) {
419
+ const [created, size] = createdResult;
420
+ oneSize = size;
421
+ oneItem = created;
422
+ } else {
423
+ oneSize = ViewPageSize.line;
424
+ oneItem = createdResult;
425
+ }
426
+ } else {
427
+ const [itemData, itemLabel, gridProps, size] = getItemField(
428
+ app,
429
+ field,
430
+ data
431
+ );
432
+
433
+ // Some callback function may return '' instead of undefined
434
+ if (itemData == null || itemData === "") continue;
435
+
436
+ oneSize = size;
437
+ oneItem = (
438
+ <ViewPageGridItem
439
+ {...gridProps}
440
+ key={i}
441
+ data={itemData}
442
+ label={itemLabel}
443
+ />
444
+ );
445
+ }
446
+
447
+ // Max lines
448
+ if (maxItems > 0) {
449
+ const itemSize = getItemSize(bp, oneSize);
450
+ if (maxItems < validItems + itemSize) {
451
+ fieldIndexRef.current = i;
452
+ break;
453
+ } else {
454
+ items.push(oneItem);
455
+ validItems += itemSize;
456
+ }
457
+ } else {
458
+ items.push(oneItem);
459
+ }
460
+ }
461
+
462
+ if (maxItems === 0) {
463
+ fieldIndexRef.current = 0;
464
+ } else {
465
+ fieldIndexRef.current = i;
466
+ }
467
+
468
+ return items;
469
+ },
470
+ [app, refresh, fields, data, bp]
471
+ );
472
+
301
473
  React.useEffect(() => {
302
474
  const refreshHandler: RefreshHandler = async () => {
303
475
  await refresh();
@@ -337,44 +509,44 @@ export function ViewPage<T extends DataTypes.StringRecord>(
337
509
  }
338
510
  />
339
511
  )}
512
+ {titleBar && titleBar(data)}
340
513
  <Grid2
341
514
  container
342
515
  justifyContent="left"
343
- spacing={spacing}
344
516
  className="ET-ViewPage"
345
517
  ref={gridRef}
518
+ spacing={spacing}
346
519
  sx={{
347
520
  ".MuiTypography-subtitle2": {
348
521
  fontWeight: "bold"
349
522
  }
350
523
  }}
351
524
  >
352
- {fields.map((field, index) => {
353
- // Get data
354
- if (typeof field === "function") {
355
- // Most flexible way, do whatever you want
356
- return field(data, refresh);
357
- }
358
-
359
- const [itemData, itemLabel, gridProps] = getItemField(
360
- app,
361
- field,
362
- data
363
- );
364
-
365
- // Some callback function may return '' instead of undefined
366
- if (itemData == null || itemData === "") return undefined;
367
-
368
- // Layout
369
- return (
370
- <ViewPageGridItem
371
- {...gridProps}
372
- key={index}
373
- data={itemData}
374
- label={itemLabel}
375
- />
376
- );
377
- })}
525
+ {leftContainer && (
526
+ <React.Fragment>
527
+ <Grid2
528
+ container
529
+ className="ET-ViewPage-LeftContainer"
530
+ spacing={spacing}
531
+ size={size}
532
+ {...leftContainerPropsRest}
533
+ >
534
+ {leftContainer(data)}
535
+ </Grid2>
536
+ <Grid2
537
+ container
538
+ className="ET-ViewPage-LeftOthers"
539
+ spacing={spacing}
540
+ size={ViewPageSize.matchSize(size)}
541
+ >
542
+ {createFields(
543
+ data,
544
+ leftContainerLines * (12 - getItemSize(bp, size))
545
+ )}
546
+ </Grid2>
547
+ </React.Fragment>
548
+ )}
549
+ {createFields(data)}
378
550
  </Grid2>
379
551
  {actions !== null && (
380
552
  <Stack
@@ -0,0 +1,17 @@
1
+ import { Breakpoint, useMediaQuery, useTheme } from "@mui/material";
2
+
3
+ /**
4
+ * Hook to get the current breakpoint
5
+ * @returns The current breakpoint
6
+ */
7
+ export function useCurrentBreakpoint(): Breakpoint {
8
+ const theme = useTheme();
9
+ const items: (Breakpoint | null)[] = [
10
+ useMediaQuery(theme.breakpoints.down("xs")) ? "xs" : null,
11
+ useMediaQuery(theme.breakpoints.between("xs", "sm")) ? "sm" : null,
12
+ useMediaQuery(theme.breakpoints.between("sm", "md")) ? "md" : null,
13
+ useMediaQuery(theme.breakpoints.between("md", "lg")) ? "lg" : null,
14
+ useMediaQuery(theme.breakpoints.up("lg")) ? "xl" : null
15
+ ];
16
+ return items.find((item) => item != null) ?? "lg";
17
+ }