jsbox-cview 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/components/alert/input-alert.ts +73 -0
  4. package/components/alert/login-alert.ts +75 -0
  5. package/components/alert/plain-alert.ts +49 -0
  6. package/components/alert/uialert.ts +110 -0
  7. package/components/artificial-flowlayout.ts +321 -0
  8. package/components/base.ts +47 -0
  9. package/components/custom-navigation-bar.ts +570 -0
  10. package/components/dialogs/dialog-sheet.ts +87 -0
  11. package/components/dialogs/form-dialog.ts +23 -0
  12. package/components/dialogs/list-dialog.ts +87 -0
  13. package/components/dialogs/text-dialog.ts +34 -0
  14. package/components/dynamic-itemsize-matrix.ts +190 -0
  15. package/components/dynamic-preference-listview.ts +691 -0
  16. package/components/dynamic-rowheight-list.ts +62 -0
  17. package/components/enhanced-imageview.ts +128 -0
  18. package/components/image-pager.ts +177 -0
  19. package/components/page-control.ts +91 -0
  20. package/components/pageviewer-titlebar.ts +170 -0
  21. package/components/pageviewer.ts +124 -0
  22. package/components/rotating-view.ts +126 -0
  23. package/components/searchbar.ts +373 -0
  24. package/components/sheet.ts +113 -0
  25. package/components/single-views.ts +828 -0
  26. package/components/spinners/loading-double-rings.ts +121 -0
  27. package/components/spinners/loading-dual-ring.ts +90 -0
  28. package/components/spinners/loading-wedges.ts +112 -0
  29. package/components/spinners/spinner-androidstyle.ts +264 -0
  30. package/components/static-preference-listview.ts +991 -0
  31. package/components/symbol-button.ts +105 -0
  32. package/components/tabbar.ts +451 -0
  33. package/controller/base-controller.ts +216 -0
  34. package/controller/controller-router.ts +74 -0
  35. package/controller/pageviewer-controller.ts +86 -0
  36. package/controller/presented-page-controller.ts +57 -0
  37. package/controller/splitview-controller.ts +323 -0
  38. package/controller/tabbar-controller.ts +99 -0
  39. package/package.json +23 -0
  40. package/test.ts +0 -0
  41. package/tsconfig.json +121 -0
  42. package/utils/colors.ts +13 -0
  43. package/utils/cvid.ts +34 -0
  44. package/utils/l10n.ts +42 -0
  45. package/utils/path.ts +100 -0
  46. package/utils/rect.ts +67 -0
  47. package/utils/uitools.ts +117 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Gandum2077
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # JSBox-CView
2
+
3
+ 为 JSBox 设计的微型框架。CView 主要实现 MVC 架构中 View、Controller 两部分。
4
+
5
+ CView 的含义是“组件化视图”。设计目的是:
6
+
7
+ - 通过组件化的方式,将 JSBox view 的定义和实例绑定,简化使用
8
+ - 方便地创建自定义组件
9
+
10
+ ## View
11
+
12
+ CView 的视图组件是非侵入式的。换言之,你可以全部使用 CView 开发,也可以只使用你喜欢的某一个组件或方法。
13
+
14
+ 它实现了 JSBox 原本的视图组件,同时增加了一些新的自定义组件。通过继承和组合,还可以创建更多的自定义组件。
15
+
16
+ ## Controller
17
+
18
+ View 组件是收敛的,而 Controller 负责页面的构成和更新。
19
+ 它可以实现一些常用的页面构建形式,比如底部 Tab 分页,左侧滑动分页,弹出式页面等。
@@ -0,0 +1,73 @@
1
+ /**
2
+ * # CView InputAlert
3
+ *
4
+ * 显示一个输入框提示
5
+ */
6
+
7
+ import {
8
+ UIAlertActionStyle,
9
+ UIAlertControllerStyle,
10
+ UIAlertAction,
11
+ UIAlertController
12
+ } from "./uialert";
13
+
14
+ import { l10n } from "../../utils/l10n";
15
+
16
+ export function inputAlert({
17
+ title = "",
18
+ message = "",
19
+ text = "",
20
+ placeholder,
21
+ type = 0,
22
+ secure = false,
23
+ cancelText = l10n("CANCEL"),
24
+ confirmText = l10n("OK")
25
+ }: {
26
+ title?: string;
27
+ message?: string;
28
+ text?: string;
29
+ placeholder?: string;
30
+ type?: number;
31
+ secure?: boolean;
32
+ cancelText?: string;
33
+ confirmText?: string;
34
+ }): Promise<string> {
35
+ return new Promise((resolve, reject) => {
36
+ const alertVC = new UIAlertController(
37
+ title,
38
+ message,
39
+ UIAlertControllerStyle.Alert
40
+ );
41
+ alertVC.addTextField({
42
+ placeholder,
43
+ text,
44
+ type,
45
+ secure,
46
+ events: {
47
+ shouldReturn: () => {
48
+ const input = alertVC.getText(0);
49
+ const isValid = input.length > 0;
50
+ return isValid;
51
+ }
52
+ }
53
+ });
54
+
55
+ alertVC.addAction(
56
+ new UIAlertAction(cancelText, UIAlertActionStyle.Destructive, cancelEvent)
57
+ );
58
+ alertVC.addAction(
59
+ new UIAlertAction(confirmText, UIAlertActionStyle.Default, confirmEvent)
60
+ );
61
+ alertVC.present();
62
+
63
+ function confirmEvent() {
64
+ const input: string = alertVC.getText(0);
65
+ resolve(input);
66
+ }
67
+ function cancelEvent() {
68
+ reject("cancel");
69
+ }
70
+ });
71
+ }
72
+
73
+
@@ -0,0 +1,75 @@
1
+ /**
2
+ * # CView LoginAlert
3
+ *
4
+ * 显示一个登录输入框提示
5
+ */
6
+
7
+ import {
8
+ UIAlertActionStyle,
9
+ UIAlertControllerStyle,
10
+ UIAlertAction,
11
+ UIAlertController
12
+ } from "./uialert";
13
+
14
+ import { l10n } from "../../utils/l10n";
15
+
16
+ export function loginAlert({
17
+ title = "",
18
+ message = "",
19
+ placeholder1 = "",
20
+ placeholder2 = "",
21
+ cancelText = l10n("CANCEL"),
22
+ confirmText = l10n("OK")
23
+ }: {
24
+ title?: string;
25
+ message?: string;
26
+ placeholder1?: string;
27
+ placeholder2?: string;
28
+ cancelText?: string;
29
+ confirmText?: string;
30
+ } = {}): Promise<{ username: string; password: string }> {
31
+ return new Promise((resolve, reject) => {
32
+ const alertVC = new UIAlertController(
33
+ title,
34
+ message,
35
+ UIAlertControllerStyle.Alert
36
+ );
37
+
38
+ alertVC.addTextField({
39
+ placeholder: placeholder1
40
+ });
41
+
42
+ alertVC.addTextField({
43
+ placeholder: placeholder2,
44
+ secure: true,
45
+ events: {
46
+ shouldReturn: () => {
47
+ const username = alertVC.getText(0);
48
+ const password = alertVC.getText(1);
49
+ const isValid = username.length > 0 && password.length > 0;
50
+ return isValid;
51
+ }
52
+ }
53
+ });
54
+
55
+ alertVC.addAction(
56
+ new UIAlertAction(cancelText, UIAlertActionStyle.Destructive, cancelEvent)
57
+ );
58
+ alertVC.addAction(
59
+ new UIAlertAction(confirmText, UIAlertActionStyle.Default, confirmEvent)
60
+ );
61
+ alertVC.present();
62
+
63
+ function confirmEvent() {
64
+ const username = alertVC.getText(0);
65
+ const password = alertVC.getText(1);
66
+ resolve({
67
+ username,
68
+ password
69
+ });
70
+ }
71
+ function cancelEvent() {
72
+ reject("cancel");
73
+ }
74
+ });
75
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * # CView PlainAlert
3
+ *
4
+ * 显示一个文字提示
5
+ */
6
+
7
+ import {
8
+ UIAlertActionStyle,
9
+ UIAlertControllerStyle,
10
+ UIAlertAction,
11
+ UIAlertController
12
+ } from "./uialert";
13
+
14
+ import { l10n } from "../../utils/l10n";
15
+
16
+ export function plainAlert({
17
+ title = "",
18
+ message = "",
19
+ cancelText = l10n("CANCEL"),
20
+ confirmText = l10n("OK")
21
+ }: {
22
+ title?: string;
23
+ message?: string;
24
+ cancelText?: string;
25
+ confirmText?: string;
26
+ } = {}): Promise<string> {
27
+ return new Promise((resolve, reject) => {
28
+ const alertVC = new UIAlertController(
29
+ title,
30
+ message,
31
+ UIAlertControllerStyle.Alert
32
+ );
33
+
34
+ alertVC.addAction(
35
+ new UIAlertAction(cancelText, UIAlertActionStyle.Destructive, cancelEvent)
36
+ );
37
+ alertVC.addAction(
38
+ new UIAlertAction(confirmText, UIAlertActionStyle.Default, confirmEvent)
39
+ );
40
+ alertVC.present();
41
+
42
+ function confirmEvent() {
43
+ resolve("ok");
44
+ }
45
+ function cancelEvent() {
46
+ reject("cancel");
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Alert的基础类
3
+ */
4
+
5
+ export const UIAlertActionStyle = {
6
+ Default: 0,
7
+ Cancel: 1,
8
+ Destructive: 2
9
+ };
10
+
11
+ export const UIAlertControllerStyle = {
12
+ ActionSheet: 0,
13
+ Alert: 1
14
+ };
15
+
16
+ export class UIAlertAction {
17
+ title: string;
18
+ style: number;
19
+ instance: any;
20
+
21
+ constructor(title: string, style = UIAlertActionStyle.Default, handler: Function) {
22
+ this.title = title;
23
+ this.style = style;
24
+ this.instance = $objc("UIAlertAction").$actionWithTitle_style_handler(
25
+ title,
26
+ style,
27
+ $block("void, UIAlertAction *", () => {
28
+ if (handler) {
29
+ handler(this);
30
+ }
31
+ })
32
+ );
33
+ }
34
+ }
35
+
36
+ export class UIAlertController {
37
+ title: string;
38
+ message: string;
39
+ style: number;
40
+ instance: any;
41
+ constructor(title: string, message: string, style = UIAlertControllerStyle.ActionSheet) {
42
+ this.title = title;
43
+ this.message = message;
44
+ this.style = style;
45
+ this.instance = $objc(
46
+ "UIAlertController"
47
+ ).$alertControllerWithTitle_message_preferredStyle(title, message, style);
48
+ }
49
+
50
+ addAction(action: UIAlertAction) {
51
+ this.instance.$addAction(action.instance);
52
+ }
53
+
54
+ addTextField(options: any) {
55
+ this.instance.$addTextFieldWithConfigurationHandler(
56
+ $block("void, UITextField *", (textField: any) => {
57
+ textField.$setClearButtonMode(1);
58
+
59
+ if (options.type) {
60
+ textField.$setKeyboardType(options.type);
61
+ }
62
+ if (options.placeholder) {
63
+ textField.$setPlaceholder(options.placeholder);
64
+ }
65
+ if (options.text) {
66
+ textField.$setText(options.text);
67
+ }
68
+ if (options.textColor) {
69
+ textField.$setTextColor(options.textColor.ocValue());
70
+ }
71
+ if (options.font) {
72
+ textField.$setFont(options.font.ocValue());
73
+ }
74
+ if (options.align) {
75
+ textField.$setTextAlignment(options.align);
76
+ }
77
+ if (options.secure) {
78
+ textField.$setSecureTextEntry(true);
79
+ }
80
+ if (options.events) {
81
+ const events = options.events;
82
+ textField.$setDelegate(
83
+ $delegate({
84
+ type: "UITextFieldDelegate",
85
+ events: {
86
+ "textFieldShouldReturn:": (textField: any) => {
87
+ if (events.shouldReturn) {
88
+ return events.shouldReturn();
89
+ } else {
90
+ return true;
91
+ }
92
+ }
93
+ }
94
+ })
95
+ );
96
+ }
97
+ })
98
+ );
99
+ }
100
+
101
+ getText(index: number) {
102
+ const textField = this.instance.$textFields().$objectAtIndex(index);
103
+ const text = textField.$text();
104
+ return text.jsValue();
105
+ }
106
+
107
+ present() {
108
+ this.instance.$show();
109
+ }
110
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * # cview ArtificialFlowlayout
3
+ *
4
+ * 仿制的 flowlayout
5
+ * 通过在右侧插入空白的 item,从而作出假象的左对齐。
6
+ *
7
+ * 已知问题:
8
+ *
9
+ * - 当某一行中所占用的总宽度恰好使得右侧的剩余宽度在 1 spacing 到 2 spacing 之间,此时无法插入空白 item,这一行的 spacing 将会被拉宽
10
+ * - 不可以依赖 indexPath,请将原数据和此 CView 的数据分离,通过 data 中加入标记的方法来定位的原数据
11
+ *
12
+ * !!! layout 当中必须有关于 height 的约束 !!!
13
+ *
14
+ * - columns 不定,item 宽度不相等但高度固定,spacing 固定,左对齐,自动换行
15
+ * - 不可滚动,会自动调整自身的高度,因而拥有方法 heightToWidth(width: number): number 用于事前确定其应有的高度
16
+ * 事件 heightChanged: (cview, height) => void 用于高度变更时回调
17
+ * - 此控件不能直接指定 props.data,而是需要指定 sections,并且每个 item 都需要改为{data: any, width: number} 其中 data 代表原 item 的内容,width 代表其应有的宽度
18
+ * - 如果 item.width > frame.width - 2 spacing, 那么生成的对应 item 将单独占用一行,其宽度为 frame.width - 2 spacing
19
+ * -
20
+ *
21
+ * 特别参数
22
+ *
23
+ * - sections: {title: string, items: {data: any, width: number}[]}[] 即使只有单个 section 也必须用多 sections 的写法
24
+ * 此参数可以在 cview.view 生成后重新写入
25
+ *
26
+ * props:
27
+ *
28
+ * - itemHeight 默认为 40
29
+ * - spacing 默认为 5
30
+ * - scrollEnabled 固定为 false
31
+ *
32
+ * 除了 props: data 和 events: itemSize 不可用,其他均和 matrix 一致
33
+ *
34
+ * methods
35
+ *
36
+ * - heightToWidth(width: number): number
37
+ * - getSectionHeights(width: number): number[]
38
+ * - reload()
39
+ */
40
+
41
+ import { Base } from "./base";
42
+ import { Matrix } from "./single-views";
43
+ import { cvid } from "../utils/cvid";
44
+
45
+ interface FlowlayoutSection {
46
+ title?: string;
47
+ items: {
48
+ data: { [key: string]: any };
49
+ width: number;
50
+ }[];
51
+ }
52
+
53
+ interface InnerSection {
54
+ title?: string;
55
+ items: {
56
+ data: { [key: string]: any };
57
+ width: number;
58
+ blank: boolean;
59
+ realIndex: number;
60
+ }[];
61
+ }
62
+
63
+ export class ArtificialFlowlayout extends Base<UIView, UiTypes.ViewOptions> {
64
+ private _sections: FlowlayoutSection[]; // 原数据,不变动,不包含空白占位的item
65
+ private _innerSections: InnerSection[]; // 带空白占位的item
66
+ private _props: UiTypes.MatrixProps;
67
+ private _cellRootViewId: string;
68
+ private _width: number;
69
+ private _height: number;
70
+ private _matrix: Matrix;
71
+ _defineView: () => UiTypes.ViewOptions;
72
+
73
+ /**
74
+ * 创建一个人工流式布局实例。
75
+ * @param sections - 此控件不能直接指定 props.data,而是需要指定 sections,
76
+ * 并且每个 item 都需要改为{data: any, width: number} 其中 data 代表原 item 的内容,width 代表其应有的宽度
77
+ * @param props - 矩阵的属性。不可指定columns,可以指定spacing,itemHeight,template。scrollEnabled固定为false。
78
+ * @param layout - 视图的布局函数。
79
+ * @param events - 事件处理程序的对象。
80
+ * - didSelect: (cview, indexPath, data): void - 其中indexPath是原sections的indexPath,data是原item的内容。
81
+ * - didLongPress: (cview, indexPath, data): void
82
+ * - heightChanged: (cview, height): void
83
+ *
84
+ * @method set sections - 设置 sections。
85
+ * @method get sections - 获取 sections。
86
+ * @method heightToWidth - 根据宽度计算高度.
87
+ *
88
+ */
89
+ constructor({ sections, props, layout, events }: {
90
+ sections: FlowlayoutSection[];
91
+ props: UiTypes.MatrixProps;
92
+ layout: (make: MASConstraintMaker, view: UIView) => void;
93
+ events: {
94
+ didSelect?: (sender: ArtificialFlowlayout, indexPath: NSIndexPath, data: object) => void;
95
+ didLongPress?: (sender: ArtificialFlowlayout, indexPath: NSIndexPath, data: object) => void;
96
+ heightChanged?: (sender: ArtificialFlowlayout, height: number) => void;
97
+ }
98
+ }) {
99
+ super();
100
+ this._sections = sections;
101
+ this._innerSections = []
102
+ this._props = {
103
+ spacing: 5,
104
+ itemHeight: 40,
105
+ ...props,
106
+ scrollEnabled: false,
107
+ columns: undefined
108
+ };
109
+ this._cellRootViewId = cvid.newId;
110
+ this._width = 300;
111
+ this._height = 0;
112
+ this._props.template = this._addCellIdForTemplate(this._props.template);
113
+ this._matrix = new Matrix({
114
+ props: {
115
+ ...this._props,
116
+ itemHeight: undefined
117
+ },
118
+ layout: $layout.fill,
119
+ events: {
120
+ itemSize: (sender, indexPath) => {
121
+ const item = this._innerSections[indexPath.section].items[indexPath.item];
122
+ return $size(Math.max(item.width, 0), this._props.itemHeight || 40);
123
+ },
124
+ didSelect: (sender, indexPath, data) => {
125
+ const item = this._innerSections[indexPath.section].items[indexPath.item];
126
+ if (item.blank) return;
127
+ const realIndexPath = $indexPath(indexPath.section, item.realIndex);
128
+ if (events.didSelect) events.didSelect(this, realIndexPath, data);
129
+ },
130
+ didLongPress: (sender, indexPath, data) => {
131
+ const item = this._innerSections[indexPath.section].items[indexPath.item];
132
+ if (item.blank) return;
133
+ const realIndexPath = $indexPath(indexPath.section, item.realIndex);
134
+ if (events.didSelect) events.didSelect(this, realIndexPath, data);
135
+ }
136
+ }
137
+ })
138
+ this._defineView = () => {
139
+ return {
140
+ type: "view",
141
+ props: {
142
+ id: this.id
143
+ },
144
+ layout: layout,
145
+ events: {
146
+ layoutSubviews: sender => {
147
+ sender.relayout();
148
+ this._width = sender.frame.width;
149
+ this._innerSections = this._addBlankItem(this._sections, this._width, this._props.spacing || 5);
150
+ const height = this._calculateSectionsHeight(
151
+ this._innerSections,
152
+ this._width,
153
+ this._props.spacing || 5,
154
+ this._props.itemHeight || 40
155
+ );
156
+ this.view.updateLayout((make, view) => make.height.equalTo(height));
157
+ this._props.data = this._innerSections.map(n => ({
158
+ title: n.title,
159
+ items: n.items.map(n => n.data)
160
+ }));
161
+ this._matrix.view.data = this._props.data;
162
+ if (height !== this._height && events.heightChanged) events.heightChanged(this, height);
163
+ this._height = height;
164
+ }
165
+ },
166
+ views: [this._matrix.definition]
167
+ };
168
+ }
169
+ }
170
+
171
+ set sections(sections) {
172
+ this._sections = sections;
173
+ this._innerSections = this._addBlankItem(this._sections, this._width, this._props.spacing || 5);
174
+ this._props.data = this._innerSections.map(n => ({
175
+ title: n.title,
176
+ items: n.items.map(n => n.data)
177
+ }));
178
+ this._matrix.view.data = this._props.data;
179
+ }
180
+
181
+ get sections() {
182
+ return this._sections;
183
+ }
184
+
185
+ /**
186
+ * 为每个 section 的 items 添加空白占位的 item。
187
+ * @param sections - 原始数据的数组,每个元素包含一个标题和一组项目。
188
+ * @param containerWidth - 容器的宽度。
189
+ * @param spacing - 项目之间的间距。
190
+ * @returns 带有空白占位的 item 的数组。
191
+ */
192
+ protected _addBlankItem(sections: FlowlayoutSection[], containerWidth: number, spacing: number): InnerSection[] {
193
+ return sections.map((section) => {
194
+ let lineWidth = spacing // 当前行已使用的宽度
195
+ const newItems: {
196
+ data: { [key: string]: any };
197
+ width: number;
198
+ blank: boolean;
199
+ realIndex: number;
200
+ }[] = []
201
+ let index = 0
202
+ for (const item of section.items) {
203
+ const itemWidthWithSpacing = item.width + spacing
204
+ // 检查是否需要换行:当前行宽度 + 项目宽度 + 间距 > 容器宽度
205
+ if (lineWidth + itemWidthWithSpacing > containerWidth && lineWidth > spacing) {
206
+ // 插入空白项目填充剩余宽度
207
+ const blankWidth = containerWidth - lineWidth + spacing // 计算空白项目宽度
208
+ if (blankWidth > 0) { // 防止插入宽度为负的空白项目
209
+ const blankItemData: {[key: string]: any} = {}
210
+ blankItemData[this._cellRootViewId] = { hidden: true }
211
+ newItems.push({
212
+ data: blankItemData,
213
+ width: blankWidth,
214
+ blank: true,
215
+ realIndex: -1
216
+ })
217
+ // 然后换行,重置lineWidth
218
+ lineWidth = spacing + itemWidthWithSpacing
219
+ newItems.push({
220
+ data: item.data,
221
+ width: item.width,
222
+ blank: false,
223
+ realIndex: index
224
+ })
225
+ } else { // 当剩余宽度恰好大于spacing但不超过2倍spacing时,需要直接换行,但这行的spacing会被稍微拉长
226
+ // 直接换行,重置lineWidth
227
+ lineWidth = spacing + itemWidthWithSpacing
228
+ newItems.push({
229
+ data: item.data,
230
+ width: item.width,
231
+ blank: false,
232
+ realIndex: index
233
+ })
234
+ }
235
+ } else {
236
+ newItems.push({
237
+ data: item.data,
238
+ width: item.width,
239
+ blank: false,
240
+ realIndex: index
241
+ })
242
+ lineWidth += itemWidthWithSpacing
243
+ }
244
+ index++;
245
+ }
246
+ return {
247
+ title: section.title,
248
+ items: newItems,
249
+ };
250
+ });
251
+ }
252
+
253
+ /**
254
+ * 计算 sections 的高度。
255
+ * @param sections - 带有空白占位的 item 的数组。
256
+ * @param containerWidth - 容器的宽度。
257
+ * @param spacing - 项目之间的间距。
258
+ * @param itemHeight - 项目的高度。
259
+ * @returns 计算得到的高度值。
260
+ */
261
+ protected _calculateSectionsHeight(
262
+ sections: InnerSection[],
263
+ containerWidth: number,
264
+ spacing: number,
265
+ itemHeight: number
266
+ ): number {
267
+ let height = spacing // 当前行已使用的高度
268
+ const lineHeight = itemHeight + spacing // 每行的高度,包括项目高度和间距
269
+ for (const section of sections) {
270
+ let lineWidth = 0 // 当前行已使用的宽度
271
+ for (const item of section.items) {
272
+ const itemWidthWithSpacing = item.width + spacing
273
+ // 检查是否需要换行:当前行宽度 + 项目宽度 + 间距 > 容器宽度
274
+ if (lineWidth + itemWidthWithSpacing > containerWidth && lineWidth > spacing) {
275
+ // 换行,重置lineWidth
276
+ lineWidth = spacing + itemWidthWithSpacing
277
+ height += lineHeight
278
+ } else {
279
+ lineWidth += itemWidthWithSpacing
280
+ }
281
+ }
282
+ }
283
+ return height
284
+ }
285
+
286
+ private _addCellIdForTemplate(template: {
287
+ views: UiTypes.AllViewOptions[];
288
+ props?: UiTypes.BaseViewProps
289
+ }): {
290
+ props: UiTypes.BaseViewProps;
291
+ views: UiTypes.AllViewOptions[];
292
+ } {
293
+ if (!template || typeof template !== "object") throw new Error("Invalid template");
294
+ const topProps = template.props;
295
+ if (topProps && topProps.id) delete topProps.id;
296
+ return {
297
+ props: {},
298
+ views: [
299
+ {
300
+ type: "view",
301
+ props: {
302
+ ...topProps,
303
+ id: this._cellRootViewId
304
+ },
305
+ layout: $layout.fill,
306
+ views: template.views
307
+ }
308
+ ]
309
+ };
310
+ }
311
+
312
+ /**
313
+ * 根据宽度计算高度。
314
+ * @param width - 宽度值。
315
+ * @returns 计算得到的高度值。
316
+ */
317
+ heightToWidth(width: number) {
318
+ const innerSections = this._addBlankItem(this._sections, width, this._props.spacing || 5);
319
+ return this._calculateSectionsHeight(innerSections, width, this._props.spacing || 5, this._props.itemHeight || 40);
320
+ }
321
+ }