@block_factory/lib 0.0.1 → 0.0.2

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/index.d.ts CHANGED
@@ -3,8 +3,10 @@ import { Signal } from "./util/Signal";
3
3
  import { Vec2, Vec3 } from "./util/Vector";
4
4
  import { RawText } from "./util/RawText";
5
5
  import { Command } from "./util/Command";
6
+ import { Form } from "./util/Form";
7
+ import { ActionFormCtor, IButtonOptions, IActionFormResponse, IActionFormData } from "./util/FormAction";
6
8
 
7
- export { MathUtils, Signal, Vec2, Vec3, RawText, Command }
9
+ export { MathUtils, Signal, Vec2, Vec3, RawText, Command, Form, ActionFormCtor, IButtonOptions, IActionFormResponse, IActionFormData }
8
10
 
9
11
  export declare const BlockFactory: {
10
12
  MathUtils: typeof MathUtils;
@@ -13,4 +15,6 @@ export declare const BlockFactory: {
13
15
  Vec3: typeof Vec3;
14
16
  RawText: typeof RawText;
15
17
  Command: typeof Command;
18
+ Form: typeof Form;
19
+ IActionFormData: typeof IActionFormData;
16
20
  };
package/index.js CHANGED
@@ -480,8 +480,11 @@ var Command;
480
480
  ((Command2) => {
481
481
  class ICustomCommand {
482
482
  constructor() {
483
+ /** Whether cheats must be enabled */
483
484
  __publicField(this, "cheatsRequired");
485
+ /** Required command parameters */
484
486
  __publicField(this, "mandatoryParameters");
487
+ /** Optional command parameters */
485
488
  __publicField(this, "optionalParameters");
486
489
  }
487
490
  }
@@ -505,6 +508,126 @@ system.beforeEvents.startup.subscribe(
505
508
  }
506
509
  );
507
510
 
511
+ // util/Form.ts
512
+ var _Form = class _Form {
513
+ static isOccupied(player) {
514
+ return _Form.occupiedPlayers.has(player.id);
515
+ }
516
+ setOccupied(player, occupied) {
517
+ if (occupied) _Form.occupiedPlayers.add(player.id);
518
+ else _Form.occupiedPlayers.delete(player.id);
519
+ }
520
+ };
521
+ __publicField(_Form, "occupiedPlayers", /* @__PURE__ */ new Set());
522
+ __publicField(_Form, "returnText", "Back");
523
+ var Form = _Form;
524
+
525
+ // util/FormAction.ts
526
+ import { ActionFormData } from "@minecraft/server-ui";
527
+ var RETURN_ID = "d7213196-4cb6-4199-a40b-22fbf2d944ef";
528
+ var IActionFormData = class extends Form {
529
+ constructor() {
530
+ super(...arguments);
531
+ __publicField(this, "form", new ActionFormData());
532
+ __publicField(this, "_buttons", /* @__PURE__ */ new Map());
533
+ __publicField(this, "_nextButtonIndex", 0);
534
+ __publicField(this, "_id", "untitled");
535
+ }
536
+ title(titleText) {
537
+ this.form.title(titleText);
538
+ this._id = typeof titleText === "string" ? titleText : JSON.stringify(titleText);
539
+ return this;
540
+ }
541
+ body(bodyText) {
542
+ this.form.body(bodyText);
543
+ return this;
544
+ }
545
+ divider() {
546
+ this.form.divider();
547
+ return this;
548
+ }
549
+ header(text) {
550
+ this.form.header(text);
551
+ return this;
552
+ }
553
+ label(text) {
554
+ this.form.label(text);
555
+ return this;
556
+ }
557
+ button(text, options = {}) {
558
+ if (options.iconPath) this.form.button(text, options.iconPath);
559
+ else this.form.button(text);
560
+ const id = options.id ?? this._nextButtonIndex;
561
+ const allowReturn = options.allowReturn ?? true;
562
+ this._buttons.set(this._nextButtonIndex, {
563
+ id,
564
+ subForm: options.subForm,
565
+ allowReturn
566
+ });
567
+ this._nextButtonIndex++;
568
+ return this;
569
+ }
570
+ async show(player) {
571
+ if (Form.isOccupied(player)) {
572
+ throw new Error(`Player ${player.id} is already occupied by form: ${this._id}`);
573
+ }
574
+ this.setOccupied(player, true);
575
+ try {
576
+ return await this._showInternal(player, []);
577
+ } finally {
578
+ this.setOccupied(player, false);
579
+ }
580
+ }
581
+ async _showChained(player, returnStack) {
582
+ return await this._showInternal(player, returnStack);
583
+ }
584
+ _wrapResponse(response, id) {
585
+ return {
586
+ canceled: response.canceled,
587
+ cancelationReason: response.cancelationReason,
588
+ selection: response.selection,
589
+ id
590
+ };
591
+ }
592
+ async _showInternal(player, returnStack) {
593
+ this.form = new ActionFormData();
594
+ this._buttons.clear();
595
+ this._nextButtonIndex = 0;
596
+ this.build(player);
597
+ if (returnStack.length > 0) {
598
+ this.form.button(Form.returnText);
599
+ this._buttons.set(this._nextButtonIndex, {
600
+ id: RETURN_ID,
601
+ subForm: returnStack[returnStack.length - 1],
602
+ allowReturn: true
603
+ });
604
+ this._nextButtonIndex++;
605
+ }
606
+ const response = await this.form.show(player);
607
+ if (response.canceled) {
608
+ const wrapped2 = this._wrapResponse(response, void 0);
609
+ this.onCancel?.(player, wrapped2);
610
+ return wrapped2;
611
+ }
612
+ const meta = response.selection !== void 0 ? this._buttons.get(response.selection) : void 0;
613
+ if (meta?.id === RETURN_ID && meta.subForm) {
614
+ const prevCtor = returnStack[returnStack.length - 1];
615
+ const newStack = returnStack.slice(0, -1);
616
+ const prev = new prevCtor();
617
+ return await prev._showChained(player, newStack);
618
+ }
619
+ const wrapped = this._wrapResponse(response, meta?.id);
620
+ this.onSubmit(player, wrapped);
621
+ if (meta?.subForm) {
622
+ const next = new meta.subForm();
623
+ const allowReturn = meta.allowReturn ?? true;
624
+ const nextStack = allowReturn ? [...returnStack, this.constructor] : [...returnStack];
625
+ return await next._showChained(player, nextStack);
626
+ }
627
+ return wrapped;
628
+ }
629
+ };
630
+
508
631
  // index.ts
509
632
  var BlockFactory;
510
633
  ((BlockFactory2) => {
@@ -514,10 +637,14 @@ var BlockFactory;
514
637
  Vec3;
515
638
  RawText;
516
639
  Command;
640
+ Form;
641
+ IActionFormData;
517
642
  })(BlockFactory || (BlockFactory = {}));
518
643
  export {
519
644
  BlockFactory,
520
645
  Command,
646
+ Form,
647
+ IActionFormData,
521
648
  MathUtils,
522
649
  RawText,
523
650
  Signal,
package/index.ts CHANGED
@@ -3,8 +3,11 @@ import { Signal } from "./util/Signal";
3
3
  import { Vec2, Vec3 } from "./util/Vector";
4
4
  import { RawText } from "./util/RawText";
5
5
  import { Command } from "./util/Command";
6
+ import { Form } from "./util/Form";
7
+ import { ActionFormCtor, IButtonOptions, IActionFormResponse, IActionFormData } from "./util/FormAction";
8
+
9
+ export { MathUtils, Signal, Vec2, Vec3, RawText, Command, Form, ActionFormCtor, IButtonOptions, IActionFormResponse, IActionFormData };
6
10
 
7
- export { MathUtils, Signal, Vec2, Vec3, RawText, Command }
8
11
 
9
12
  export namespace BlockFactory {
10
13
  MathUtils;
@@ -13,4 +16,6 @@ export namespace BlockFactory {
13
16
  Vec3;
14
17
  RawText;
15
18
  Command;
19
+ Form;
20
+ IActionFormData;
16
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@block_factory/lib",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "description": "Typescript Library for Minecraft Bedrock Edition",
package/util/Form.d.ts CHANGED
@@ -1,75 +1,10 @@
1
- import { Player, RawMessage } from "@minecraft/server";
2
- import { ActionFormResponse, ModalFormResponse } from "@minecraft/server-ui";
3
- declare class Form {
4
- readonly isOccupied: (player: Player) => boolean;
5
- }
6
- export interface Button {
7
- indexId: string | number;
8
- text: string | RawMessage;
9
- iconPath?: string;
10
- }
11
- export interface FormActionData {
12
- response: ActionFormResponse;
13
- indexId?: string | number;
14
- }
15
- export declare class ActionForm extends Form {
16
- private readonly actionForm;
17
- private buttonMap;
18
- private buttons;
19
- private titleText;
20
- private bodyText;
21
- title(title: string | RawMessage): void;
22
- body(body: string | RawMessage): void;
23
- button(indexId: string, text: string | RawMessage, iconPath?: string): void;
24
- showForm(player: Player): Promise<FormActionData>;
25
- private generateButtons;
26
- }
27
- export interface TextField {
28
- indexId: string;
29
- label: string | RawMessage;
30
- placeholder: string | RawMessage;
31
- defaultValue?: string;
32
- }
33
- export interface Dropdown {
34
- indexId: string;
35
- label: string | RawMessage;
36
- options: string[];
37
- defaultValueIndex?: number;
38
- }
39
- export interface Slider {
40
- indexId: string;
41
- label: string | RawMessage;
42
- min: number;
43
- max: number;
44
- step: number;
45
- defaultValue?: number;
46
- }
47
- export interface Toggle {
48
- indexId: string;
49
- label: string | RawMessage;
50
- defaultValue?: boolean;
51
- }
52
- export type FormValue = string | boolean | number | undefined;
53
- export interface FormModalData {
54
- response: ModalFormResponse;
55
- indexMap?: Map<string, FormValue>;
56
- }
57
- export declare class ModalForm extends Form {
58
- private readonly modalForm;
59
- private widgitMap;
60
- private indexMap;
61
- private widgets;
62
- title(title: string | RawMessage): void;
63
- label(label: string | RawMessage): void;
64
- header(header: string | RawMessage): void;
65
- divider(): void;
66
- textField(indexId: string, label: string | RawMessage, placeholder: string | RawMessage, defaultValue?: string): void;
67
- dropdown(indexId: string, label: string | RawMessage, options: string[], defaultValueIndex?: number): void;
68
- slider(indexId: string, label: string | RawMessage, min: number, max: number, step: number, defaultValue?: number): void;
69
- toggle(indexId: string, label: string | RawMessage, defaultValue?: boolean): void;
70
- showForm(player: Player): Promise<FormModalData>;
71
- private processWidgets;
72
- private processValues;
73
- }
74
- export {};
75
- //# sourceMappingURL=Form.d.ts.map
1
+ import { Player, RawMessage } from "@minecraft/server";
2
+
3
+ export declare abstract class Form {
4
+ private static readonly occupiedPlayers: Set<string>;
5
+ static returnText: RawMessage | string;
6
+
7
+ static isOccupied(player: Player): boolean;
8
+
9
+ protected setOccupied(player: Player, occupied: boolean): void;
10
+ }
package/util/Form.ts CHANGED
@@ -1,246 +1,15 @@
1
- /*
2
- **************************************************
3
- Copyright (c) Block Factory - All rights reserved.
4
- **************************************************
5
- Author: Donthedev <https://github.com/voxeldon>
6
- **************************************************
7
- */
8
1
  import { Player, RawMessage } from "@minecraft/server";
9
- import { ActionFormData, ActionFormResponse, ModalFormData, ModalFormDataDropdownOptions, ModalFormDataSliderOptions, ModalFormDataTextFieldOptions, ModalFormDataToggleOptions, ModalFormResponse } from "@minecraft/server-ui";
10
2
 
11
- const occupiedPlayers: Set<string> = new Set();
3
+ export abstract class Form {
4
+ private static readonly occupiedPlayers: Set<string> = new Set();
5
+ public static returnText: RawMessage | string = "Back";
12
6
 
13
- class Form {
14
- public readonly isOccupied = ((player: Player): boolean => {
15
- return occupiedPlayers.has(player.id);
16
- })
17
- }
18
-
19
- export interface Button {
20
- indexId: string | number;
21
- text: string | RawMessage;
22
- iconPath?: string;
23
- }
24
-
25
- export interface FormActionData {
26
- response: ActionFormResponse;
27
- indexId?: string | number;
28
- }
29
-
30
- export class ActionForm extends Form {
31
- private readonly actionForm: ActionFormData = new ActionFormData();
32
- private buttonMap: Map<number, string | number> = new Map<number, string | number>();
33
- private buttons: Button[] = [];
34
- private titleText: string | RawMessage | undefined;
35
- private bodyText: string | RawMessage | undefined;
36
-
37
- public title(title: string | RawMessage): void {
38
- this.titleText = title;
39
- }
40
-
41
- public body(body: string | RawMessage): void {
42
- this.bodyText = body;
43
- }
44
-
45
- public button(indexId: string, text: string | RawMessage, iconPath?: string): void {
46
- this.buttons.push({ indexId, text, iconPath });
47
- }
48
-
49
- public async showForm(player: Player): Promise<FormActionData> {
50
- if (this.titleText === undefined || this.bodyText === undefined) {
51
- throw Error('Title and body must be set before showing the form');
52
- }
53
- this.actionForm.title(this.titleText);
54
- this.actionForm.body(this.bodyText);
55
- this.generateButtons();
56
-
57
- occupiedPlayers.add(player.id);
58
-
59
- const response: ActionFormResponse = await this.actionForm.show(player).finally(() => {
60
- occupiedPlayers.delete(player.id);
61
- });
62
-
63
- const return_data: FormActionData = {
64
- response: response,
65
- indexId: undefined
66
- };
67
- if (response.selection !== undefined && response.selection !== null) {
68
- const selection: string | number | undefined = this.buttonMap.get(response.selection);
69
- return_data.indexId = selection;
70
- }
71
-
72
- return return_data;
73
- }
74
-
75
- private generateButtons() {
76
- let buttonIndex: number = 0;
77
- this.buttons.forEach(button => {
78
- if (button.iconPath) {
79
- this.actionForm.button(button.text, button.iconPath);
80
- } else {
81
- this.actionForm.button(button.text);
82
- }
83
- this.buttonMap.set(buttonIndex, button.indexId);
84
- buttonIndex++;
85
- });
86
- }
87
- }
88
-
89
- enum WidgetType {
90
- TextField = 'textField',
91
- Dropdown = 'dropdown',
92
- Slider = 'slider',
93
- Toggle = 'toggle'
94
- }
95
-
96
- interface Widget {
97
- typeId: string,
98
- widget: any
99
- };
100
-
101
- export interface TextField {
102
- indexId: string,
103
- label: string | RawMessage,
104
- placeholder: string | RawMessage,
105
- defaultValue?: string
106
- }
107
-
108
- export interface Dropdown {
109
- indexId: string,
110
- label: string | RawMessage,
111
- options: string[],
112
- defaultValueIndex?: number
113
- }
114
-
115
- export interface Slider {
116
- indexId: string,
117
- label: string | RawMessage,
118
- min: number,
119
- max: number,
120
- step: number,
121
- defaultValue?: number
122
- }
123
-
124
- export interface Toggle {
125
- indexId: string,
126
- label: string | RawMessage,
127
- defaultValue?: boolean
128
- }
129
-
130
- export type FormValue = string | boolean | number | undefined;
131
-
132
- export interface FormModalData {
133
- response: ModalFormResponse;
134
- indexMap?: Map<string, FormValue>;
135
- }
136
-
137
- export class ModalForm extends Form {
138
- private readonly modalForm: ModalFormData = new ModalFormData();
139
- private widgitMap: Map<number, string> = new Map<number, string>();
140
- private indexMap: Map<string, FormValue> = new Map<string, FormValue>();
141
- private widgets: Widget[] = [];
142
-
143
- public title(title: string | RawMessage): void {
144
- this.modalForm.title(title);
145
- }
146
-
147
- public label(label: string | RawMessage): void {
148
- this.modalForm.label(label);
149
- }
150
-
151
- public header(header: string | RawMessage): void {
152
- this.modalForm.header(header);
153
- }
154
-
155
- public divider(): void {
156
- this.modalForm.divider();
157
- }
158
-
159
- public textField(indexId: string, label: string | RawMessage, placeholder: string | RawMessage, defaultValue?: string): void {
160
- this.widgets.push({ typeId: WidgetType.TextField, widget: { indexId, label, placeholder, defaultValue } });
161
- }
162
- public dropdown(indexId: string, label: string | RawMessage, options: string[], defaultValueIndex?: number): void {
163
- this.widgets.push({ typeId: WidgetType.Dropdown, widget: { indexId, label, options, defaultValueIndex } });
164
- }
165
-
166
- public slider(indexId: string, label: string | RawMessage, min: number, max: number, step: number, defaultValue?: number): void {
167
- this.widgets.push({ typeId: WidgetType.Slider, widget: { indexId, label, min, max, step, defaultValue } });
168
- }
169
-
170
- public toggle(indexId: string, label: string | RawMessage, defaultValue?: boolean): void {
171
- this.widgets.push({ typeId: WidgetType.Toggle, widget: { indexId, label, defaultValue } });
172
- }
173
-
174
- public async showForm(player: Player): Promise<FormModalData> {
175
- this.processWidgets();
176
- occupiedPlayers.add(player.id);
177
-
178
- const response: ModalFormResponse = await this.modalForm.show(player).finally(() => {
179
- occupiedPlayers.delete(player.id);
180
- });
181
-
182
- const return_data: FormModalData = {
183
- response: response,
184
- indexMap: undefined
185
- }
186
-
187
- if (response.formValues !== null && response.formValues !== undefined) {
188
- this.processValues(response.formValues);
189
- return_data.indexMap = this.indexMap;
190
- }
191
-
192
- return return_data;
193
- }
194
-
195
- private processWidgets(): void {
196
- let widgitIndex: number = 0;
197
-
198
- for (const widget of this.widgets) {
199
- if (widget.typeId === WidgetType.TextField) {
200
- const textField: TextField = widget.widget;
201
- const options: ModalFormDataTextFieldOptions = {
202
- defaultValue: textField?.defaultValue
203
- };
204
- this.modalForm.textField(textField.label, textField.placeholder, options);
205
- this.widgitMap.set(widgitIndex, textField.indexId);
206
- }
207
- else if (widget.typeId === WidgetType.Dropdown) {
208
- const dropdown: Dropdown = widget.widget;
209
- const options: ModalFormDataDropdownOptions = {
210
- defaultValueIndex: dropdown?.defaultValueIndex
211
- };
212
- this.modalForm.dropdown(dropdown.label, dropdown.options, options);
213
- this.widgitMap.set(widgitIndex, dropdown.indexId);
214
-
215
- }
216
- else if (widget.typeId === WidgetType.Slider) {
217
- const slider: Slider = widget.widget;
218
- const options: ModalFormDataSliderOptions = {
219
- defaultValue: slider?.defaultValue,
220
- valueStep: slider.step
221
- };
222
- this.modalForm.slider(slider.label, slider.min, slider.max, options);
223
- this.widgitMap.set(widgitIndex, slider.indexId);
224
- }
225
- else if (widget.typeId === WidgetType.Toggle) {
226
- const toggle: Toggle = widget.widget;
227
- const options: ModalFormDataToggleOptions = {
228
- defaultValue: toggle?.defaultValue
229
- }
230
- this.modalForm.toggle(toggle.label, options);
231
- this.widgitMap.set(widgitIndex, toggle.indexId);
232
- }
233
- widgitIndex += 1;
234
- }
7
+ public static isOccupied(player: Player): boolean {
8
+ return Form.occupiedPlayers.has(player.id);
235
9
  }
236
10
 
237
- private processValues(formValues: FormValue[]): void {
238
- let widgitIndex: number = 0;
239
- for (const i of formValues) {
240
- const value: FormValue = i?.valueOf();
241
- const indexId: string | undefined = this.widgitMap.get(widgitIndex);
242
- if (indexId) this.indexMap.set(indexId, value);
243
- widgitIndex++;
244
- }
11
+ protected setOccupied(player: Player, occupied: boolean): void {
12
+ if (occupied) Form.occupiedPlayers.add(player.id);
13
+ else Form.occupiedPlayers.delete(player.id);
245
14
  }
246
15
  }
@@ -0,0 +1,41 @@
1
+ import { Form } from "@block_factory/lib/util/Form";
2
+ import { Player, RawMessage } from "@minecraft/server";
3
+ import { ActionFormData, FormCancelationReason } from "@minecraft/server-ui";
4
+
5
+ export declare type ActionFormCtor<T extends IActionFormData = IActionFormData> = new () => T;
6
+
7
+ export declare interface IButtonOptions {
8
+ id?: string | number;
9
+ iconPath?: string;
10
+ subForm?: ActionFormCtor;
11
+ allowReturn?: boolean;
12
+ }
13
+
14
+ export declare interface IActionFormResponse {
15
+ readonly cancelationReason?: FormCancelationReason;
16
+ readonly canceled: boolean;
17
+ readonly selection?: number;
18
+ readonly id?: string | number;
19
+ }
20
+
21
+ export declare abstract class IActionFormData extends Form {
22
+ protected form: ActionFormData;
23
+
24
+ protected abstract build(player: Player): void;
25
+
26
+ protected abstract onSubmit(player: Player, response: IActionFormResponse): void;
27
+
28
+ protected onCancel?(player: Player, response: IActionFormResponse): void;
29
+
30
+ title(titleText: RawMessage | string): this;
31
+ body(bodyText: RawMessage | string): this;
32
+ divider(): this;
33
+ header(text: RawMessage | string): this;
34
+ label(text: RawMessage | string): this;
35
+
36
+ button(text: RawMessage | string, options?: IButtonOptions): this;
37
+
38
+ show(player: Player): Promise<IActionFormResponse>;
39
+
40
+ protected _showChained(player: Player, returnStack: ActionFormCtor[]): Promise<IActionFormResponse>;
41
+ }
@@ -0,0 +1,207 @@
1
+ import { Form } from "@block_factory/lib/util/Form";
2
+ import { Player, RawMessage } from "@minecraft/server";
3
+ import { ActionFormData, ActionFormResponse, FormCancelationReason } from "@minecraft/server-ui";
4
+
5
+ const RETURN_ID = "d7213196-4cb6-4199-a40b-22fbf2d944ef";
6
+
7
+ /**
8
+ * Constructor type for ActionForm classes.
9
+ * Used for sub-form navigation and chaining.
10
+ */
11
+ export type ActionFormCtor<T extends IActionFormData = IActionFormData> = new () => T;
12
+
13
+ /**
14
+ * Options used when adding a button to an ActionForm.
15
+ */
16
+ export interface IButtonOptions {
17
+ /**
18
+ * Logical identifier returned in the form response.
19
+ * If omitted, the numeric button index is used.
20
+ */
21
+ id?: string | number;
22
+
23
+ /**
24
+ * Optional icon path from a resource pack.
25
+ */
26
+ iconPath?: string;
27
+
28
+ /**
29
+ * Form class to open when this button is selected.
30
+ */
31
+ subForm?: ActionFormCtor;
32
+
33
+ /**
34
+ * Whether the sub-form is allowed to return to this form.
35
+ * Defaults to true.
36
+ */
37
+ allowReturn?: boolean;
38
+ }
39
+
40
+ type ButtonMeta = {
41
+ id: string | number;
42
+ subForm?: ActionFormCtor;
43
+ allowReturn: boolean;
44
+ };
45
+
46
+ /**
47
+ * Normalized response object returned by IActionFormData.
48
+ */
49
+ export interface IActionFormResponse {
50
+ /**
51
+ * Reason the form was canceled, if applicable.
52
+ */
53
+ readonly cancelationReason?: FormCancelationReason;
54
+
55
+ /**
56
+ * Whether the form was canceled by the player.
57
+ */
58
+ readonly canceled: boolean;
59
+
60
+ /**
61
+ * Raw button index selected by the player.
62
+ */
63
+ readonly selection?: number;
64
+
65
+ /**
66
+ * Logical button id associated with the selection.
67
+ */
68
+ readonly id?: string | number;
69
+ }
70
+
71
+ /**
72
+ * Abstract base class for ActionForm-based menus.
73
+ *
74
+ * Provides:
75
+ * - Button id mapping
76
+ * - Occupied player handling
77
+ * - Sub-form chaining with automatic Back navigation
78
+ * - Normalized response objects
79
+ */
80
+ export abstract class IActionFormData extends Form {
81
+ protected form: ActionFormData = new ActionFormData();
82
+
83
+ private _buttons = new Map<number, ButtonMeta>();
84
+ private _nextButtonIndex = 0;
85
+ private _id: string = "untitled";
86
+
87
+ protected abstract build(player: Player): void;
88
+
89
+ protected abstract onSubmit( player: Player, response: IActionFormResponse): void;
90
+
91
+ protected onCancel?(player: Player, response: IActionFormResponse): void;
92
+
93
+ public title(titleText: RawMessage | string): this {
94
+ this.form.title(titleText);
95
+ this._id = typeof titleText === "string" ? titleText : JSON.stringify(titleText);
96
+ return this;
97
+ }
98
+
99
+ public body(bodyText: RawMessage | string): this { this.form.body(bodyText); return this;}
100
+
101
+ public divider(): this { this.form.divider(); return this;}
102
+
103
+ public header(text: RawMessage | string): this { this.form.header(text); return this;}
104
+
105
+ public label(text: RawMessage | string): this { this.form.label(text); return this;}
106
+
107
+ public button(text: RawMessage | string, options: IButtonOptions = {}): this {
108
+ if (options.iconPath) this.form.button(text, options.iconPath);
109
+ else this.form.button(text);
110
+
111
+ const id: string | number = options.id ?? this._nextButtonIndex;
112
+ const allowReturn = options.allowReturn ?? true;
113
+
114
+ this._buttons.set(this._nextButtonIndex, {
115
+ id,
116
+ subForm: options.subForm,
117
+ allowReturn,
118
+ });
119
+
120
+ this._nextButtonIndex++;
121
+ return this;
122
+ }
123
+
124
+ public async show(player: Player): Promise<IActionFormResponse> {
125
+ if (Form.isOccupied(player)) {
126
+ throw new Error(`Player ${player.id} is already occupied by form: ${this._id}`);
127
+ }
128
+
129
+ this.setOccupied(player, true);
130
+
131
+ try {
132
+ return await this._showInternal(player, []);
133
+ } finally {
134
+ this.setOccupied(player, false);
135
+ }
136
+ }
137
+
138
+ protected async _showChained(player: Player,returnStack: ActionFormCtor[]): Promise<IActionFormResponse> {
139
+ return await this._showInternal(player, returnStack);
140
+ }
141
+
142
+ private _wrapResponse(response: ActionFormResponse,id?: string | number): IActionFormResponse {
143
+ return {
144
+ canceled: response.canceled,
145
+ cancelationReason: response.cancelationReason,
146
+ selection: response.selection,
147
+ id,
148
+ };
149
+ }
150
+
151
+ private async _showInternal(player: Player, returnStack: ActionFormCtor[]): Promise<IActionFormResponse> {
152
+
153
+ this.form = new ActionFormData();
154
+
155
+ this._buttons.clear();
156
+ this._nextButtonIndex = 0;
157
+
158
+ this.build(player);
159
+
160
+ if (returnStack.length > 0) {
161
+ this.form.button(Form.returnText);
162
+ this._buttons.set(this._nextButtonIndex, {
163
+ id: RETURN_ID,
164
+ subForm: returnStack[returnStack.length - 1],
165
+ allowReturn: true,
166
+ });
167
+ this._nextButtonIndex++;
168
+ }
169
+
170
+ const response = await this.form.show(player);
171
+
172
+ if (response.canceled) {
173
+ const wrapped = this._wrapResponse(response, undefined);
174
+ this.onCancel?.(player, wrapped);
175
+ return wrapped;
176
+ }
177
+
178
+ const meta =
179
+ response.selection !== undefined
180
+ ? this._buttons.get(response.selection)
181
+ : undefined;
182
+
183
+ if (meta?.id === RETURN_ID && meta.subForm) {
184
+ const prevCtor = returnStack[returnStack.length - 1];
185
+ const newStack = returnStack.slice(0, -1);
186
+ const prev = new prevCtor();
187
+ return await prev._showChained(player, newStack);
188
+ }
189
+
190
+ const wrapped = this._wrapResponse(response, meta?.id);
191
+
192
+ this.onSubmit(player, wrapped);
193
+
194
+ if (meta?.subForm) {
195
+ const next = new meta.subForm();
196
+ const allowReturn = meta.allowReturn ?? true;
197
+
198
+ const nextStack = allowReturn
199
+ ? [...returnStack, this.constructor as ActionFormCtor]
200
+ : [...returnStack];
201
+
202
+ return await next._showChained(player, nextStack);
203
+ }
204
+
205
+ return wrapped;
206
+ }
207
+ }
package/util/_Form.ts ADDED
@@ -0,0 +1,246 @@
1
+ /*
2
+ **************************************************
3
+ Copyright (c) Block Factory - All rights reserved.
4
+ **************************************************
5
+ Author: Donthedev <https://github.com/voxeldon>
6
+ **************************************************
7
+ */
8
+ import { Player, RawMessage } from "@minecraft/server";
9
+ import { ActionFormData, ActionFormResponse, ModalFormData, ModalFormDataDropdownOptions, ModalFormDataSliderOptions, ModalFormDataTextFieldOptions, ModalFormDataToggleOptions, ModalFormResponse } from "@minecraft/server-ui";
10
+
11
+ const occupiedPlayers: Set<string> = new Set();
12
+
13
+ class Form {
14
+ public readonly isOccupied = ((player: Player): boolean => {
15
+ return occupiedPlayers.has(player.id);
16
+ })
17
+ }
18
+
19
+ export interface Button {
20
+ indexId: string | number;
21
+ text: string | RawMessage;
22
+ iconPath?: string;
23
+ }
24
+
25
+ export interface FormActionData {
26
+ response: ActionFormResponse;
27
+ indexId?: string | number;
28
+ }
29
+
30
+ export class ActionForm extends Form {
31
+ private readonly actionForm: ActionFormData = new ActionFormData();
32
+ private buttonMap: Map<number, string | number> = new Map<number, string | number>();
33
+ private buttons: Button[] = [];
34
+ private titleText: string | RawMessage | undefined;
35
+ private bodyText: string | RawMessage | undefined;
36
+
37
+ public title(title: string | RawMessage): void {
38
+ this.titleText = title;
39
+ }
40
+
41
+ public body(body: string | RawMessage): void {
42
+ this.bodyText = body;
43
+ }
44
+
45
+ public button(indexId: string, text: string | RawMessage, iconPath?: string): void {
46
+ this.buttons.push({ indexId, text, iconPath });
47
+ }
48
+
49
+ public async showForm(player: Player): Promise<FormActionData> {
50
+ if (this.titleText === undefined || this.bodyText === undefined) {
51
+ throw Error('Title and body must be set before showing the form');
52
+ }
53
+ this.actionForm.title(this.titleText);
54
+ this.actionForm.body(this.bodyText);
55
+ this.generateButtons();
56
+
57
+ occupiedPlayers.add(player.id);
58
+
59
+ const response: ActionFormResponse = await this.actionForm.show(player).finally(() => {
60
+ occupiedPlayers.delete(player.id);
61
+ });
62
+
63
+ const return_data: FormActionData = {
64
+ response: response,
65
+ indexId: undefined
66
+ };
67
+ if (response.selection !== undefined && response.selection !== null) {
68
+ const selection: string | number | undefined = this.buttonMap.get(response.selection);
69
+ return_data.indexId = selection;
70
+ }
71
+
72
+ return return_data;
73
+ }
74
+
75
+ private generateButtons() {
76
+ let buttonIndex: number = 0;
77
+ this.buttons.forEach(button => {
78
+ if (button.iconPath) {
79
+ this.actionForm.button(button.text, button.iconPath);
80
+ } else {
81
+ this.actionForm.button(button.text);
82
+ }
83
+ this.buttonMap.set(buttonIndex, button.indexId);
84
+ buttonIndex++;
85
+ });
86
+ }
87
+ }
88
+
89
+ enum WidgetType {
90
+ TextField = 'textField',
91
+ Dropdown = 'dropdown',
92
+ Slider = 'slider',
93
+ Toggle = 'toggle'
94
+ }
95
+
96
+ interface Widget {
97
+ typeId: string,
98
+ widget: any
99
+ };
100
+
101
+ export interface TextField {
102
+ indexId: string,
103
+ label: string | RawMessage,
104
+ placeholder: string | RawMessage,
105
+ defaultValue?: string
106
+ }
107
+
108
+ export interface Dropdown {
109
+ indexId: string,
110
+ label: string | RawMessage,
111
+ options: string[],
112
+ defaultValueIndex?: number
113
+ }
114
+
115
+ export interface Slider {
116
+ indexId: string,
117
+ label: string | RawMessage,
118
+ min: number,
119
+ max: number,
120
+ step: number,
121
+ defaultValue?: number
122
+ }
123
+
124
+ export interface Toggle {
125
+ indexId: string,
126
+ label: string | RawMessage,
127
+ defaultValue?: boolean
128
+ }
129
+
130
+ export type FormValue = string | boolean | number | undefined;
131
+
132
+ export interface FormModalData {
133
+ response: ModalFormResponse;
134
+ indexMap?: Map<string, FormValue>;
135
+ }
136
+
137
+ export class ModalForm extends Form {
138
+ private readonly modalForm: ModalFormData = new ModalFormData();
139
+ private widgitMap: Map<number, string> = new Map<number, string>();
140
+ private indexMap: Map<string, FormValue> = new Map<string, FormValue>();
141
+ private widgets: Widget[] = [];
142
+
143
+ public title(title: string | RawMessage): void {
144
+ this.modalForm.title(title);
145
+ }
146
+
147
+ public label(label: string | RawMessage): void {
148
+ this.modalForm.label(label);
149
+ }
150
+
151
+ public header(header: string | RawMessage): void {
152
+ this.modalForm.header(header);
153
+ }
154
+
155
+ public divider(): void {
156
+ this.modalForm.divider();
157
+ }
158
+
159
+ public textField(indexId: string, label: string | RawMessage, placeholder: string | RawMessage, defaultValue?: string): void {
160
+ this.widgets.push({ typeId: WidgetType.TextField, widget: { indexId, label, placeholder, defaultValue } });
161
+ }
162
+ public dropdown(indexId: string, label: string | RawMessage, options: string[], defaultValueIndex?: number): void {
163
+ this.widgets.push({ typeId: WidgetType.Dropdown, widget: { indexId, label, options, defaultValueIndex } });
164
+ }
165
+
166
+ public slider(indexId: string, label: string | RawMessage, min: number, max: number, step: number, defaultValue?: number): void {
167
+ this.widgets.push({ typeId: WidgetType.Slider, widget: { indexId, label, min, max, step, defaultValue } });
168
+ }
169
+
170
+ public toggle(indexId: string, label: string | RawMessage, defaultValue?: boolean): void {
171
+ this.widgets.push({ typeId: WidgetType.Toggle, widget: { indexId, label, defaultValue } });
172
+ }
173
+
174
+ public async showForm(player: Player): Promise<FormModalData> {
175
+ this.processWidgets();
176
+ occupiedPlayers.add(player.id);
177
+
178
+ const response: ModalFormResponse = await this.modalForm.show(player).finally(() => {
179
+ occupiedPlayers.delete(player.id);
180
+ });
181
+
182
+ const return_data: FormModalData = {
183
+ response: response,
184
+ indexMap: undefined
185
+ }
186
+
187
+ if (response.formValues !== null && response.formValues !== undefined) {
188
+ this.processValues(response.formValues);
189
+ return_data.indexMap = this.indexMap;
190
+ }
191
+
192
+ return return_data;
193
+ }
194
+
195
+ private processWidgets(): void {
196
+ let widgitIndex: number = 0;
197
+
198
+ for (const widget of this.widgets) {
199
+ if (widget.typeId === WidgetType.TextField) {
200
+ const textField: TextField = widget.widget;
201
+ const options: ModalFormDataTextFieldOptions = {
202
+ defaultValue: textField?.defaultValue
203
+ };
204
+ this.modalForm.textField(textField.label, textField.placeholder, options);
205
+ this.widgitMap.set(widgitIndex, textField.indexId);
206
+ }
207
+ else if (widget.typeId === WidgetType.Dropdown) {
208
+ const dropdown: Dropdown = widget.widget;
209
+ const options: ModalFormDataDropdownOptions = {
210
+ defaultValueIndex: dropdown?.defaultValueIndex
211
+ };
212
+ this.modalForm.dropdown(dropdown.label, dropdown.options, options);
213
+ this.widgitMap.set(widgitIndex, dropdown.indexId);
214
+
215
+ }
216
+ else if (widget.typeId === WidgetType.Slider) {
217
+ const slider: Slider = widget.widget;
218
+ const options: ModalFormDataSliderOptions = {
219
+ defaultValue: slider?.defaultValue,
220
+ valueStep: slider.step
221
+ };
222
+ this.modalForm.slider(slider.label, slider.min, slider.max, options);
223
+ this.widgitMap.set(widgitIndex, slider.indexId);
224
+ }
225
+ else if (widget.typeId === WidgetType.Toggle) {
226
+ const toggle: Toggle = widget.widget;
227
+ const options: ModalFormDataToggleOptions = {
228
+ defaultValue: toggle?.defaultValue
229
+ }
230
+ this.modalForm.toggle(toggle.label, options);
231
+ this.widgitMap.set(widgitIndex, toggle.indexId);
232
+ }
233
+ widgitIndex += 1;
234
+ }
235
+ }
236
+
237
+ private processValues(formValues: FormValue[]): void {
238
+ let widgitIndex: number = 0;
239
+ for (const i of formValues) {
240
+ const value: FormValue = i?.valueOf();
241
+ const indexId: string | undefined = this.widgitMap.get(widgitIndex);
242
+ if (indexId) this.indexMap.set(indexId, value);
243
+ widgitIndex++;
244
+ }
245
+ }
246
+ }