@vnejs/plugins.views.settings 0.2.1 → 0.2.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.
@@ -1,4 +1,5 @@
1
1
  import { ModuleController } from "@vnejs/module.components";
2
+ import type { SettingsField } from "@vnejs/contracts.views.settings";
2
3
  import type { SettingsViewPluginConstants, SettingsViewPluginEvents, SettingsViewPluginParams, SettingsViewPluginSettings } from "../types.js";
3
4
  import type { SettingsViewPluginState, SettingsViewTabContent, SettingsViewTabPayload } from "../utils/settings.js";
4
5
  export declare class SettingsViewController extends ModuleController<SettingsViewPluginEvents, SettingsViewPluginConstants, SettingsViewPluginSettings, SettingsViewPluginParams, SettingsViewPluginState> {
@@ -13,15 +14,20 @@ export declare class SettingsViewController extends ModuleController<SettingsVie
13
14
  tabsContent: {
14
15
  [k: string]: SettingsViewTabContent;
15
16
  };
16
- fields: import("@vnejs/contracts.views.settings").SettingsField[];
17
+ fields: SettingsField[];
17
18
  locs: Record<string, string> | undefined;
18
19
  showTextExample: boolean;
19
20
  currentItemInner: number;
20
21
  };
21
22
  onShowFields: (payload?: SettingsViewTabPayload) => Promise<void>;
22
23
  onTabSet: (payload?: SettingsViewTabPayload) => Promise<void | unknown[]>;
23
- onArrowLeft: () => false | Promise<unknown[]> | undefined;
24
- onArrowRight: () => false | Promise<unknown[]> | undefined;
24
+ getFocusedField: () => SettingsField | undefined;
25
+ innerIndexOfField: (field?: SettingsField) => number;
26
+ optionIndexOf: (length: number, index?: number) => number;
27
+ shiftOptions: (direction: -1 | 1) => void;
28
+ setCurrentItem: (value: number, defaultValue: number) => Promise<void>;
29
+ onArrowLeft: () => void | false;
30
+ onArrowRight: () => void | false;
25
31
  onInteract: () => void;
26
32
  controls: {
27
33
  abstract_interact: () => undefined;
@@ -45,17 +45,63 @@ export class SettingsViewController extends ModuleController {
45
45
  }
46
46
  return this.updateStateAndView(next);
47
47
  };
48
- onArrowLeft = () => {
49
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
48
+ getFocusedField = () => {
49
+ const { currentItem, fields = [] } = this.state;
50
50
  if (currentItem == null || !fields.length)
51
51
  return;
52
- const { settingKey = "", step = 0, min = 0, type = "", options = [] } = fields[currentItem];
52
+ return fields[currentItem];
53
+ };
54
+ innerIndexOfField = (field) => {
55
+ const options = field?.options ?? [];
56
+ if (!options.length)
57
+ return 0;
58
+ const settingKey = field?.settingKey;
53
59
  if (!settingKey)
60
+ return 0;
61
+ const index = options.findIndex((option) => option.value === this.shared.settings[settingKey]);
62
+ return index >= 0 ? index : 0;
63
+ };
64
+ optionIndexOf = (length, index = 0) => {
65
+ if (!length)
66
+ return 0;
67
+ return ((index % length) + length) % length;
68
+ };
69
+ shiftOptions = (direction) => {
70
+ const field = this.getFocusedField();
71
+ const options = field?.options ?? [];
72
+ if (!options.length)
54
73
  return;
55
- if (options?.length) {
56
- const newValue = (currentItemInner === 0 ? options.length : (currentItemInner ?? 0)) - 1;
57
- return this.updateStateAndViewFast({ currentItemInner: newValue });
74
+ const currentItemInner = this.optionIndexOf(options.length, (this.state.currentItemInner ?? 0) + direction);
75
+ const option = options[currentItemInner];
76
+ const settingKey = field?.settingKey;
77
+ void this.updateStateAndViewFast({ currentItemInner });
78
+ if (settingKey && option && !option.action) {
79
+ void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
58
80
  }
81
+ };
82
+ setCurrentItem = async (value, defaultValue) => {
83
+ if (this.currentItem === null) {
84
+ this.currentItem = defaultValue;
85
+ }
86
+ else {
87
+ this.currentItem = value;
88
+ if (this.currentItem > this.maxCurrentItem)
89
+ this.currentItem = 0;
90
+ if (this.currentItem < 0)
91
+ this.currentItem = this.maxCurrentItem;
92
+ }
93
+ const currentItemInner = this.innerIndexOfField(this.state.fields?.[this.currentItem]);
94
+ await this.updateStateAndViewFast({ currentItem: this.currentItem, currentItemInner });
95
+ };
96
+ onArrowLeft = () => {
97
+ const field = this.getFocusedField();
98
+ if (!field)
99
+ return;
100
+ if (field.options?.length)
101
+ return this.shiftOptions(-1);
102
+ const { settingKey = "", step = 0, min = 0, type = "" } = field;
103
+ if (!settingKey)
104
+ return;
59
105
  if (type === "toggle") {
60
106
  return this.shared.settings[settingKey] === true && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: false });
61
107
  }
@@ -65,15 +111,14 @@ export class SettingsViewController extends ModuleController {
65
111
  }
66
112
  };
67
113
  onArrowRight = () => {
68
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
69
- if (currentItem == null || !fields.length)
114
+ const field = this.getFocusedField();
115
+ if (!field)
70
116
  return;
71
- const { settingKey = "", step = 0, max = 0, type = "", options = [] } = fields[currentItem];
117
+ if (field.options?.length)
118
+ return this.shiftOptions(1);
119
+ const { settingKey = "", step = 0, max = 0, type = "" } = field;
72
120
  if (!settingKey)
73
121
  return;
74
- if (options?.length) {
75
- return this.updateStateAndViewFast({ currentItemInner: ((currentItemInner ?? 0) + 1) % options.length });
76
- }
77
122
  if (type === "toggle") {
78
123
  return this.shared.settings[settingKey] === false && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: true });
79
124
  }
@@ -83,16 +128,23 @@ export class SettingsViewController extends ModuleController {
83
128
  }
84
129
  };
85
130
  onInteract = () => {
86
- const { currentItemInner = 0, currentItem, fields = [], locs = {} } = this.state;
87
- if (currentItem == null || !fields.length)
131
+ const field = this.getFocusedField();
132
+ if (!field)
88
133
  return;
89
- const { settingKey = "", type = "", options = [] } = fields[currentItem];
134
+ const { settingKey = "", type = "", options = [] } = field;
135
+ const { locs = {} } = this.state;
136
+ if (options?.length) {
137
+ const option = options[this.optionIndexOf(options.length, this.state.currentItemInner ?? 0)];
138
+ if (!option)
139
+ return;
140
+ if (option.action)
141
+ return this.PARAMS.SETTINGS_VIEW.ACTIONS[option.action](this, locs);
142
+ if (settingKey)
143
+ return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
144
+ return;
145
+ }
90
146
  if (!settingKey)
91
147
  return;
92
- if (options?.length)
93
- return options[currentItemInner ?? 0].action
94
- ? this.PARAMS.SETTINGS_VIEW.ACTIONS[options[currentItemInner ?? 0].action](this, locs)
95
- : void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: options[currentItemInner ?? 0].value });
96
148
  if (type === "toggle")
97
149
  return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: !this.shared.settings[settingKey] });
98
150
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vnejs/plugins.views.settings",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,5 @@
1
1
  import { ModuleController } from "@vnejs/module.components";
2
+ import type { SettingsField } from "@vnejs/contracts.views.settings";
2
3
 
3
4
  import type { SettingsViewPluginConstants, SettingsViewPluginEvents, SettingsViewPluginParams, SettingsViewPluginSettings } from "../types.js";
4
5
  import type { SettingsViewPluginState, SettingsViewTabContent, SettingsViewTabPayload } from "../utils/settings.js";
@@ -72,20 +73,73 @@ export class SettingsViewController extends ModuleController<
72
73
  return this.updateStateAndView(next);
73
74
  };
74
75
 
75
- onArrowLeft = () => {
76
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
76
+ getFocusedField = () => {
77
+ const { currentItem, fields = [] } = this.state;
77
78
 
78
79
  if (currentItem == null || !fields.length) return;
79
80
 
80
- const { settingKey = "", step = 0, min = 0, type = "", options = [] } = fields[currentItem];
81
- if (!settingKey) return;
81
+ return fields[currentItem];
82
+ };
82
83
 
83
- if (options?.length) {
84
- const newValue = (currentItemInner === 0 ? options.length : (currentItemInner ?? 0)) - 1;
84
+ innerIndexOfField = (field?: SettingsField) => {
85
+ const options = field?.options ?? [];
86
+ if (!options.length) return 0;
87
+
88
+ const settingKey = field?.settingKey;
89
+ if (!settingKey) return 0;
90
+
91
+ const index = options.findIndex((option) => option.value === this.shared.settings[settingKey]);
92
+
93
+ return index >= 0 ? index : 0;
94
+ };
95
+
96
+ optionIndexOf = (length: number, index = 0) => {
97
+ if (!length) return 0;
98
+
99
+ return ((index % length) + length) % length;
100
+ };
85
101
 
86
- return this.updateStateAndViewFast({ currentItemInner: newValue });
102
+ shiftOptions = (direction: -1 | 1) => {
103
+ const field = this.getFocusedField();
104
+ const options = field?.options ?? [];
105
+
106
+ if (!options.length) return;
107
+
108
+ const currentItemInner = this.optionIndexOf(options.length, (this.state.currentItemInner ?? 0) + direction);
109
+ const option = options[currentItemInner];
110
+ const settingKey = field?.settingKey;
111
+
112
+ void this.updateStateAndViewFast({ currentItemInner });
113
+
114
+ if (settingKey && option && !option.action) {
115
+ void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
116
+ }
117
+ };
118
+
119
+ setCurrentItem = async (value: number, defaultValue: number) => {
120
+ if (this.currentItem === null) {
121
+ this.currentItem = defaultValue;
122
+ } else {
123
+ this.currentItem = value;
124
+ if (this.currentItem > this.maxCurrentItem) this.currentItem = 0;
125
+ if (this.currentItem < 0) this.currentItem = this.maxCurrentItem;
87
126
  }
88
127
 
128
+ const currentItemInner = this.innerIndexOfField(this.state.fields?.[this.currentItem]);
129
+
130
+ await this.updateStateAndViewFast({ currentItem: this.currentItem, currentItemInner });
131
+ };
132
+
133
+ onArrowLeft = () => {
134
+ const field = this.getFocusedField();
135
+
136
+ if (!field) return;
137
+
138
+ if (field.options?.length) return this.shiftOptions(-1);
139
+
140
+ const { settingKey = "", step = 0, min = 0, type = "" } = field;
141
+ if (!settingKey) return;
142
+
89
143
  if (type === "toggle") {
90
144
  return this.shared.settings[settingKey] === true && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: false });
91
145
  }
@@ -98,16 +152,14 @@ export class SettingsViewController extends ModuleController<
98
152
  };
99
153
 
100
154
  onArrowRight = () => {
101
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
155
+ const field = this.getFocusedField();
102
156
 
103
- if (currentItem == null || !fields.length) return;
157
+ if (!field) return;
104
158
 
105
- const { settingKey = "", step = 0, max = 0, type = "", options = [] } = fields[currentItem];
106
- if (!settingKey) return;
159
+ if (field.options?.length) return this.shiftOptions(1);
107
160
 
108
- if (options?.length) {
109
- return this.updateStateAndViewFast({ currentItemInner: ((currentItemInner ?? 0) + 1) % options.length });
110
- }
161
+ const { settingKey = "", step = 0, max = 0, type = "" } = field;
162
+ if (!settingKey) return;
111
163
 
112
164
  if (type === "toggle") {
113
165
  return this.shared.settings[settingKey] === false && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: true });
@@ -121,17 +173,25 @@ export class SettingsViewController extends ModuleController<
121
173
  };
122
174
 
123
175
  onInteract = () => {
124
- const { currentItemInner = 0, currentItem, fields = [], locs = {} } = this.state;
176
+ const field = this.getFocusedField();
125
177
 
126
- if (currentItem == null || !fields.length) return;
178
+ if (!field) return;
127
179
 
128
- const { settingKey = "", type = "", options = [] } = fields[currentItem];
129
- if (!settingKey) return;
180
+ const { settingKey = "", type = "", options = [] } = field;
181
+ const { locs = {} } = this.state;
182
+
183
+ if (options?.length) {
184
+ const option = options[this.optionIndexOf(options.length, this.state.currentItemInner ?? 0)];
130
185
 
131
- if (options?.length)
132
- return options[currentItemInner ?? 0].action
133
- ? this.PARAMS.SETTINGS_VIEW.ACTIONS[options[currentItemInner ?? 0].action!](this, locs)
134
- : void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: options[currentItemInner ?? 0].value });
186
+ if (!option) return;
187
+
188
+ if (option.action) return this.PARAMS.SETTINGS_VIEW.ACTIONS[option.action](this, locs);
189
+ if (settingKey) return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
190
+
191
+ return;
192
+ }
193
+
194
+ if (!settingKey) return;
135
195
 
136
196
  if (type === "toggle") return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: !this.shared.settings[settingKey] });
137
197
  };
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import { spyEvent } from "@vnejs/test-utils";
4
4
  import { SUBSCRIBE_EVENTS as SETTINGS_EVENTS } from "@vnejs/contracts.core.settings";
@@ -90,4 +90,151 @@ describe("SettingsViewController", () => {
90
90
  expect((module as SettingsViewController).state.tabKey).toBe("audio");
91
91
  expect(Object.keys((module as SettingsViewController).state.tabsContent ?? {}).sort()).toEqual(["audio", "general"]);
92
92
  });
93
+
94
+ it("ARROW_RIGHT on selector_texts cycles inner and SET", async () => {
95
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController, {
96
+ shared: { settings: { "test.lang": "ru" } },
97
+ });
98
+
99
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
100
+ tab: {
101
+ titleLocKey: "general",
102
+ fields: [
103
+ {
104
+ settingKey: "test.lang",
105
+ type: "selector_texts",
106
+ options: [
107
+ { text: "ru", value: "ru" },
108
+ { text: "en", value: "en" },
109
+ { text: "de", value: "de" },
110
+ ],
111
+ },
112
+ ],
113
+ },
114
+ });
115
+
116
+ const controller = module as SettingsViewController;
117
+
118
+ controller.currentItem = 0;
119
+ controller.updateState({ currentItem: 0, currentItemInner: 0 });
120
+
121
+ const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
122
+
123
+ controller.onArrowRight();
124
+
125
+ expect(controller.state.currentItemInner).toBe(1);
126
+ expect(settingsSet).toHaveBeenCalledExactlyOnceWith({ name: "test.lang", value: "en" });
127
+ });
128
+
129
+ it("ARROW_LEFT on selector_texts wraps inner and SET", async () => {
130
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController, {
131
+ shared: { settings: { "test.lang": "ru" } },
132
+ });
133
+
134
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
135
+ tab: {
136
+ titleLocKey: "general",
137
+ fields: [
138
+ {
139
+ settingKey: "test.lang",
140
+ type: "selector_texts",
141
+ options: [
142
+ { text: "ru", value: "ru" },
143
+ { text: "en", value: "en" },
144
+ { text: "de", value: "de" },
145
+ ],
146
+ },
147
+ ],
148
+ },
149
+ });
150
+
151
+ const controller = module as SettingsViewController;
152
+
153
+ controller.currentItem = 0;
154
+ controller.updateState({ currentItem: 0, currentItemInner: 0 });
155
+
156
+ const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
157
+
158
+ controller.onArrowLeft();
159
+
160
+ expect(controller.state.currentItemInner).toBe(2);
161
+ expect(settingsSet).toHaveBeenCalledExactlyOnceWith({ name: "test.lang", value: "de" });
162
+ });
163
+
164
+ it("ARROW_* on actions without settingKey only moves inner", async () => {
165
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController);
166
+ const controller = module as SettingsViewController;
167
+ const doA = vi.fn();
168
+ const doB = vi.fn();
169
+
170
+ controller.PARAMS.SETTINGS_VIEW.ACTIONS.do_a = doA;
171
+ controller.PARAMS.SETTINGS_VIEW.ACTIONS.do_b = doB;
172
+
173
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
174
+ tab: {
175
+ titleLocKey: "general",
176
+ fields: [
177
+ {
178
+ type: "actions",
179
+ options: [
180
+ { locKey: "a", action: "do_a" },
181
+ { locKey: "b", action: "do_b" },
182
+ ],
183
+ },
184
+ ],
185
+ },
186
+ });
187
+
188
+ controller.currentItem = 0;
189
+ controller.updateState({ currentItem: 0, currentItemInner: 0 });
190
+
191
+ const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
192
+
193
+ controller.onArrowLeft();
194
+
195
+ expect(controller.state.currentItemInner).toBe(1);
196
+ expect(doA).not.toHaveBeenCalled();
197
+ expect(doB).not.toHaveBeenCalled();
198
+ expect(settingsSet).not.toHaveBeenCalled();
199
+
200
+ controller.onInteract();
201
+
202
+ expect(doB).toHaveBeenCalledOnce();
203
+ expect(doA).not.toHaveBeenCalled();
204
+ });
205
+
206
+ it("setCurrentItem syncs currentItemInner to the current option", async () => {
207
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController, {
208
+ shared: { settings: { "test.lang": "en" } },
209
+ });
210
+
211
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
212
+ tab: {
213
+ titleLocKey: "general",
214
+ fields: [
215
+ { settingKey: "a", type: "toggle" },
216
+ {
217
+ settingKey: "test.lang",
218
+ type: "selector_texts",
219
+ options: [
220
+ { text: "ru", value: "ru" },
221
+ { text: "en", value: "en" },
222
+ { text: "de", value: "de" },
223
+ ],
224
+ },
225
+ ],
226
+ },
227
+ });
228
+
229
+ const controller = module as SettingsViewController;
230
+
231
+ controller.currentItem = 0;
232
+ controller.updateState({ currentItem: 0, currentItemInner: 99 });
233
+
234
+ await controller.setCurrentItem(1, 0);
235
+
236
+ expect(controller.currentItem).toBe(1);
237
+ expect(controller.state.currentItem).toBe(1);
238
+ expect(controller.state.currentItemInner).toBe(1);
239
+ });
93
240
  });