@vnejs/plugins.views.settings 0.2.1 → 0.2.3

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/dist/index.js CHANGED
@@ -2,5 +2,6 @@ import "@vnejs/contracts.views.settings";
2
2
  import { regPlugin } from "@vnejs/shared";
3
3
  import { PARAMS, PLUGIN_NAME, SUBSCRIBE_EVENTS } from "@vnejs/contracts.views.settings";
4
4
  import { SettingsViewController } from "./modules/controller.js";
5
+ import { SettingsViewArrows } from "./modules/arrows.js";
5
6
  import { SettingsView } from "./modules/view.js";
6
- regPlugin(PLUGIN_NAME, { events: SUBSCRIBE_EVENTS, params: PARAMS }, [SettingsViewController, SettingsView]);
7
+ regPlugin(PLUGIN_NAME, { events: SUBSCRIBE_EVENTS, params: PARAMS }, [SettingsViewController, SettingsViewArrows, SettingsView]);
@@ -0,0 +1,18 @@
1
+ import type { SettingsField } from "@vnejs/contracts.views.settings";
2
+ import { ModuleControllerArrowsVertical } from "@vnejs/module.controller.arrows.vertical";
3
+ import type { SettingsViewPluginConstants, SettingsViewPluginEvents, SettingsViewPluginParams, SettingsViewPluginSettings } from "../types.js";
4
+ import type { SettingsViewTabPayload } from "../utils/settings.js";
5
+ export declare class SettingsViewArrows extends ModuleControllerArrowsVertical<SettingsViewPluginEvents, SettingsViewPluginConstants, SettingsViewPluginSettings, SettingsViewPluginParams> {
6
+ name: string;
7
+ EVENT_SHOW: "vne:settings_view:show";
8
+ EVENT_HIDE: "vne:settings_view:hide";
9
+ EVENT_STATE_UPDATE: "vne:settings_view:state_update";
10
+ EVENT_UPDATE_FAST: "vne:settings_view:update_fast";
11
+ CONTROLS_ZINDEX: number;
12
+ fields: SettingsField[];
13
+ subscribe: () => void;
14
+ onShow: (payload?: SettingsViewTabPayload) => Promise<unknown[]> | undefined;
15
+ onTabSet: (payload?: SettingsViewTabPayload) => void;
16
+ getMaxCurrentItem: () => number;
17
+ syncFields: (payload?: SettingsViewTabPayload) => void;
18
+ }
@@ -0,0 +1,34 @@
1
+ import { ModuleControllerArrowsVertical } from "@vnejs/module.controller.arrows.vertical";
2
+ export class SettingsViewArrows extends ModuleControllerArrowsVertical {
3
+ name = "settings.fields.controller.arrows";
4
+ EVENT_SHOW = this.EVENTS.SETTINGS_VIEW.SHOW;
5
+ EVENT_HIDE = this.EVENTS.SETTINGS_VIEW.HIDE;
6
+ EVENT_STATE_UPDATE = this.EVENTS.SETTINGS_VIEW.STATE_UPDATE;
7
+ EVENT_UPDATE_FAST = this.EVENTS.SETTINGS_VIEW.UPDATE_FAST;
8
+ CONTROLS_ZINDEX = this.PARAMS.SETTINGS_VIEW.ZINDEX;
9
+ fields = [];
10
+ subscribe = () => {
11
+ this.on(this.EVENT_SHOW, this.onShow);
12
+ this.on(this.EVENT_HIDE, this.onHide);
13
+ this.on(this.EVENTS.SETTINGS_VIEW.TAB_SET, this.onTabSet);
14
+ };
15
+ onShow = (payload = {}) => {
16
+ this.syncFields(payload);
17
+ this.currentItem = null;
18
+ return this.emit(this.EVENTS.CONTROLS.PUSH, {
19
+ key: this.name,
20
+ controls: this.controls,
21
+ index: this.CONTROLS_ZINDEX + 1,
22
+ checkNext: true,
23
+ });
24
+ };
25
+ onTabSet = (payload = {}) => {
26
+ this.syncFields(payload);
27
+ this.currentItem = null;
28
+ this.emitCurrentItem();
29
+ };
30
+ getMaxCurrentItem = () => Math.max((this.fields.length ?? 1) - 1, 0);
31
+ syncFields = (payload = {}) => {
32
+ this.fields = payload.tab?.fields ?? [];
33
+ };
34
+ }
@@ -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,22 +14,25 @@ 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
+ onStateUpdate: (payload?: Partial<SettingsViewPluginState>) => void;
25
+ getFocusedField: () => SettingsField | undefined;
26
+ innerIndexOfField: (field?: SettingsField) => number;
27
+ optionIndexOf: (length: number, index?: number) => number;
28
+ shiftOptions: (direction: -1 | 1) => void;
29
+ onArrowLeft: () => void | false;
30
+ onArrowRight: () => void | false;
25
31
  onInteract: () => void;
26
32
  controls: {
27
33
  abstract_interact: () => undefined;
28
34
  abstract_arrow_left: () => undefined;
29
35
  abstract_arrow_right: () => undefined;
30
- abstract_arrow_up: () => Promise<void>;
31
- abstract_arrow_bottom: () => Promise<void>;
32
36
  };
33
37
  controlsCheckNext: boolean;
34
38
  subscribe: () => void;
@@ -13,7 +13,6 @@ export class SettingsViewController extends ModuleController {
13
13
  const { tab, locs } = payload;
14
14
  const fields = tab?.fields ?? [];
15
15
  const key = this.tabKeyOf(payload);
16
- this.maxCurrentItem = Math.max((fields.length ?? 1) - 1, 0);
17
16
  const tabsContent = payload.tabs
18
17
  ? Object.fromEntries(Object.entries(payload.tabs).map(([tabKey, settingsTab]) => [tabKey, this.tabContentOf(settingsTab, locs)]))
19
18
  : { ...(this.state.tabsContent ?? {}), [key]: this.tabContentOf(tab, locs) };
@@ -32,8 +31,7 @@ export class SettingsViewController extends ModuleController {
32
31
  if (!this.state.isShow)
33
32
  return this.onShow(payload);
34
33
  const isForce = Boolean(payload.isForce || this.shared.viewForceAnimationSources?.isNotEmpty());
35
- this.currentItem = null;
36
- const next = { ...this.applyTab(payload), currentItem: null };
34
+ const next = { ...this.applyTab(payload), currentArrowVerticalItem: null };
37
35
  if (isForce)
38
36
  return this.updateStateAndViewForce(next);
39
37
  const key = next.tabKey;
@@ -45,17 +43,54 @@ export class SettingsViewController extends ModuleController {
45
43
  }
46
44
  return this.updateStateAndView(next);
47
45
  };
48
- onArrowLeft = () => {
49
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
50
- if (currentItem == null || !fields.length)
46
+ onStateUpdate = (payload = {}) => {
47
+ const currentArrowVerticalItem = payload.currentArrowVerticalItem;
48
+ const field = currentArrowVerticalItem == null ? undefined : this.state.fields?.[currentArrowVerticalItem];
49
+ this.updateState({ ...payload, currentItemInner: this.innerIndexOfField(field) });
50
+ };
51
+ getFocusedField = () => {
52
+ const { currentArrowVerticalItem, fields = [] } = this.state;
53
+ if (currentArrowVerticalItem == null || !fields.length)
51
54
  return;
52
- const { settingKey = "", step = 0, min = 0, type = "", options = [] } = fields[currentItem];
55
+ return fields[currentArrowVerticalItem];
56
+ };
57
+ innerIndexOfField = (field) => {
58
+ const options = field?.options ?? [];
59
+ if (!options.length)
60
+ return 0;
61
+ const settingKey = field?.settingKey;
53
62
  if (!settingKey)
63
+ return 0;
64
+ const index = options.findIndex((option) => option.value === this.shared.settings[settingKey]);
65
+ return index >= 0 ? index : 0;
66
+ };
67
+ optionIndexOf = (length, index = 0) => {
68
+ if (!length)
69
+ return 0;
70
+ return ((index % length) + length) % length;
71
+ };
72
+ shiftOptions = (direction) => {
73
+ const field = this.getFocusedField();
74
+ const options = field?.options ?? [];
75
+ if (!options.length)
54
76
  return;
55
- if (options?.length) {
56
- const newValue = (currentItemInner === 0 ? options.length : (currentItemInner ?? 0)) - 1;
57
- return this.updateStateAndViewFast({ currentItemInner: newValue });
77
+ const currentItemInner = this.optionIndexOf(options.length, (this.state.currentItemInner ?? 0) + direction);
78
+ const option = options[currentItemInner];
79
+ const settingKey = field?.settingKey;
80
+ void this.updateStateAndViewFast({ currentItemInner });
81
+ if (settingKey && option && !option.action) {
82
+ void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
58
83
  }
84
+ };
85
+ onArrowLeft = () => {
86
+ const field = this.getFocusedField();
87
+ if (!field)
88
+ return;
89
+ if (field.options?.length)
90
+ return this.shiftOptions(-1);
91
+ const { settingKey = "", step = 0, min = 0, type = "" } = field;
92
+ if (!settingKey)
93
+ return;
59
94
  if (type === "toggle") {
60
95
  return this.shared.settings[settingKey] === true && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: false });
61
96
  }
@@ -65,15 +100,14 @@ export class SettingsViewController extends ModuleController {
65
100
  }
66
101
  };
67
102
  onArrowRight = () => {
68
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
69
- if (currentItem == null || !fields.length)
103
+ const field = this.getFocusedField();
104
+ if (!field)
70
105
  return;
71
- const { settingKey = "", step = 0, max = 0, type = "", options = [] } = fields[currentItem];
106
+ if (field.options?.length)
107
+ return this.shiftOptions(1);
108
+ const { settingKey = "", step = 0, max = 0, type = "" } = field;
72
109
  if (!settingKey)
73
110
  return;
74
- if (options?.length) {
75
- return this.updateStateAndViewFast({ currentItemInner: ((currentItemInner ?? 0) + 1) % options.length });
76
- }
77
111
  if (type === "toggle") {
78
112
  return this.shared.settings[settingKey] === false && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: true });
79
113
  }
@@ -83,16 +117,23 @@ export class SettingsViewController extends ModuleController {
83
117
  }
84
118
  };
85
119
  onInteract = () => {
86
- const { currentItemInner = 0, currentItem, fields = [], locs = {} } = this.state;
87
- if (currentItem == null || !fields.length)
120
+ const field = this.getFocusedField();
121
+ if (!field)
122
+ return;
123
+ const { settingKey = "", type = "", options = [] } = field;
124
+ const { locs = {} } = this.state;
125
+ if (options?.length) {
126
+ const option = options[this.optionIndexOf(options.length, this.state.currentItemInner ?? 0)];
127
+ if (!option)
128
+ return;
129
+ if (option.action)
130
+ return this.PARAMS.SETTINGS_VIEW.ACTIONS[option.action](this, locs);
131
+ if (settingKey)
132
+ return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
88
133
  return;
89
- const { settingKey = "", type = "", options = [] } = fields[currentItem];
134
+ }
90
135
  if (!settingKey)
91
136
  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
137
  if (type === "toggle")
97
138
  return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: !this.shared.settings[settingKey] });
98
139
  };
@@ -100,14 +141,14 @@ export class SettingsViewController extends ModuleController {
100
141
  [this.CONST.CONTROLS.BUTTONS.INTERACT]: () => void this.onInteract(),
101
142
  [this.CONST.CONTROLS.BUTTONS.ARROW_LEFT]: () => void this.onArrowLeft(),
102
143
  [this.CONST.CONTROLS.BUTTONS.ARROW_RIGHT]: () => void this.onArrowRight(),
103
- [this.CONST.CONTROLS.BUTTONS.ARROW_UP]: this.decCurrentItem,
104
- [this.CONST.CONTROLS.BUTTONS.ARROW_BOTTOM]: this.incCurrentItem,
105
144
  };
106
145
  controlsCheckNext = true;
107
146
  subscribe = () => {
108
147
  this.on(this.EVENTS.SETTINGS_VIEW.SHOW, this.onShowFields);
109
148
  this.on(this.EVENTS.SETTINGS_VIEW.HIDE, this.onHide);
110
149
  this.on(this.EVENTS.SETTINGS_VIEW.TAB_SET, this.onTabSet);
150
+ this.on(this.EVENTS.SETTINGS_VIEW.STATE_UPDATE, this.onStateUpdate);
151
+ this.on(this.EVENTS.SETTINGS_VIEW.UPDATE_FAST, this.updateViewFast);
111
152
  this.on(this.EVENTS.SETTINGS.CHANGED, this.onSettingChange);
112
153
  this.on(this.EVENTS.COMPONENTS.EXTERNAL_UPDATE, this.onSettingChange);
113
154
  };
@@ -13,7 +13,7 @@ export type SettingsViewPluginState = {
13
13
  fields?: SettingsField[];
14
14
  locs?: Record<string, string>;
15
15
  showTextExample?: boolean;
16
- currentItem?: number | null;
16
+ currentArrowVerticalItem?: number | null;
17
17
  currentItemInner?: number | null;
18
18
  };
19
19
  export type SettingsViewTabPayload = {
@@ -3,7 +3,7 @@ import { createElement as _createElement } from "react";
3
3
  import { View, createRenderFunc, useIsForceHook, useMemo, useStoreState } from "@vnejs/uis.react";
4
4
  import { SettingsTabContent } from "./SettingsTabContent.js";
5
5
  const SettingsViewComponent = ({ store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS }) => {
6
- const { isShow = false, isForce = false, locs = {}, tabsContent = {}, tabKey = "", currentItem = null, currentItemInner = null, } = useStoreState(store, onMount);
6
+ const { isShow = false, isForce = false, locs = {}, tabsContent = {}, tabKey = "", currentArrowVerticalItem = null, currentItemInner = null, } = useStoreState(store, onMount);
7
7
  const isRealForce = useIsForceHook(isForce);
8
8
  const propsByView = PARAMS.SETTINGS_VIEW.VIEW_PROPS;
9
9
  const propsView = useMemo(() => ({
@@ -12,7 +12,7 @@ const SettingsViewComponent = ({ store, onMount, PARAMS, emit, EVENTS, shared, C
12
12
  isForce: isRealForce,
13
13
  transition: isRealForce ? 0 : PARAMS.SETTINGS_VIEW.TRANSITION,
14
14
  }), [isShow, isRealForce, propsByView.view, PARAMS.SETTINGS_VIEW.TRANSITION]);
15
- const propsTab = useMemo(() => ({ store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isForce: isRealForce, currentItem, currentItemInner, locs }), [store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isRealForce, currentItem, currentItemInner, locs]);
15
+ const propsTab = useMemo(() => ({ store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isForce: isRealForce, currentItem: currentArrowVerticalItem, currentItemInner, locs }), [store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isRealForce, currentArrowVerticalItem, currentItemInner, locs]);
16
16
  return (_jsx(View, { ...propsView, children: Object.entries(tabsContent).map(([key, content]) => (_createElement(SettingsTabContent, { ...propsTab, key: key, isShow: tabKey === key, fields: content.fields, showTextExample: content.showTextExample, locs: content.locs ?? locs }))) }));
17
17
  };
18
18
  export const render = createRenderFunc(SettingsViewComponent);
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.3",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,6 +36,7 @@
36
36
  "@vnejs/helpers": "~0.2.0",
37
37
  "@vnejs/module.core": "~0.2.0",
38
38
  "@vnejs/module.components": "~0.2.0",
39
+ "@vnejs/module.controller.arrows.vertical": "~0.2.0",
39
40
  "@vnejs/contracts.controls": "~0.2.0",
40
41
  "@vnejs/contracts.core.settings": "~0.2.0",
41
42
  "@vnejs/contracts.settings.textsize": "~0.2.0",
@@ -55,6 +56,7 @@
55
56
  "@vnejs/helpers": "~0.2.0",
56
57
  "@vnejs/module.core": "~0.2.0",
57
58
  "@vnejs/module.components": "~0.2.0",
59
+ "@vnejs/module.controller.arrows.vertical": "~0.2.0",
58
60
  "@vnejs/shared": "~0.2.0",
59
61
  "@vnejs/sources-set": "~0.2.0",
60
62
  "@vnejs/uis.react": "~0.2.0"
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { regPlugin } from "@vnejs/shared";
4
4
  import { PARAMS, PLUGIN_NAME, SUBSCRIBE_EVENTS } from "@vnejs/contracts.views.settings";
5
5
 
6
6
  import { SettingsViewController } from "./modules/controller.js";
7
+ import { SettingsViewArrows } from "./modules/arrows.js";
7
8
  import { SettingsView } from "./modules/view.js";
8
9
 
9
- regPlugin(PLUGIN_NAME, { events: SUBSCRIBE_EVENTS, params: PARAMS }, [SettingsViewController, SettingsView]);
10
+ regPlugin(PLUGIN_NAME, { events: SUBSCRIBE_EVENTS, params: PARAMS }, [SettingsViewController, SettingsViewArrows, SettingsView]);
@@ -0,0 +1,54 @@
1
+ import type { ControlsPushPayload } from "@vnejs/contracts.controls";
2
+ import type { SettingsField } from "@vnejs/contracts.views.settings";
3
+ import { ModuleControllerArrowsVertical } from "@vnejs/module.controller.arrows.vertical";
4
+
5
+ import type { SettingsViewPluginConstants, SettingsViewPluginEvents, SettingsViewPluginParams, SettingsViewPluginSettings } from "../types.js";
6
+ import type { SettingsViewTabPayload } from "../utils/settings.js";
7
+
8
+ export class SettingsViewArrows extends ModuleControllerArrowsVertical<
9
+ SettingsViewPluginEvents,
10
+ SettingsViewPluginConstants,
11
+ SettingsViewPluginSettings,
12
+ SettingsViewPluginParams
13
+ > {
14
+ name = "settings.fields.controller.arrows";
15
+
16
+ EVENT_SHOW = this.EVENTS.SETTINGS_VIEW.SHOW;
17
+ EVENT_HIDE = this.EVENTS.SETTINGS_VIEW.HIDE;
18
+ EVENT_STATE_UPDATE = this.EVENTS.SETTINGS_VIEW.STATE_UPDATE;
19
+ EVENT_UPDATE_FAST = this.EVENTS.SETTINGS_VIEW.UPDATE_FAST;
20
+
21
+ CONTROLS_ZINDEX = this.PARAMS.SETTINGS_VIEW.ZINDEX;
22
+
23
+ fields: SettingsField[] = [];
24
+
25
+ subscribe = () => {
26
+ this.on(this.EVENT_SHOW, this.onShow);
27
+ this.on(this.EVENT_HIDE, this.onHide);
28
+ this.on(this.EVENTS.SETTINGS_VIEW.TAB_SET, this.onTabSet);
29
+ };
30
+
31
+ onShow = (payload: SettingsViewTabPayload = {}) => {
32
+ this.syncFields(payload);
33
+ this.currentItem = null;
34
+
35
+ return this.emit(this.EVENTS.CONTROLS.PUSH, {
36
+ key: this.name,
37
+ controls: this.controls,
38
+ index: this.CONTROLS_ZINDEX + 1,
39
+ checkNext: true,
40
+ } satisfies ControlsPushPayload);
41
+ };
42
+
43
+ onTabSet = (payload: SettingsViewTabPayload = {}) => {
44
+ this.syncFields(payload);
45
+ this.currentItem = null;
46
+ this.emitCurrentItem();
47
+ };
48
+
49
+ getMaxCurrentItem = () => Math.max((this.fields.length ?? 1) - 1, 0);
50
+
51
+ syncFields = (payload: SettingsViewTabPayload = {}) => {
52
+ this.fields = payload.tab?.fields ?? [];
53
+ };
54
+ }
@@ -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";
@@ -28,8 +29,6 @@ export class SettingsViewController extends ModuleController<
28
29
  const fields = tab?.fields ?? [];
29
30
  const key = this.tabKeyOf(payload);
30
31
 
31
- this.maxCurrentItem = Math.max((fields.length ?? 1) - 1, 0);
32
-
33
32
  const tabsContent = payload.tabs
34
33
  ? Object.fromEntries(
35
34
  Object.entries(payload.tabs).map(([tabKey, settingsTab]) => [tabKey, this.tabContentOf(settingsTab, locs)]),
@@ -54,9 +53,7 @@ export class SettingsViewController extends ModuleController<
54
53
 
55
54
  const isForce = Boolean(payload.isForce || this.shared.viewForceAnimationSources?.isNotEmpty());
56
55
 
57
- this.currentItem = null;
58
-
59
- const next = { ...this.applyTab(payload), currentItem: null as null };
56
+ const next = { ...this.applyTab(payload), currentArrowVerticalItem: null as null };
60
57
 
61
58
  if (isForce) return this.updateStateAndViewForce(next);
62
59
 
@@ -72,19 +69,65 @@ export class SettingsViewController extends ModuleController<
72
69
  return this.updateStateAndView(next);
73
70
  };
74
71
 
75
- onArrowLeft = () => {
76
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
72
+ onStateUpdate = (payload: Partial<SettingsViewPluginState> = {}) => {
73
+ const currentArrowVerticalItem = payload.currentArrowVerticalItem;
74
+ const field = currentArrowVerticalItem == null ? undefined : this.state.fields?.[currentArrowVerticalItem];
77
75
 
78
- if (currentItem == null || !fields.length) return;
76
+ this.updateState({ ...payload, currentItemInner: this.innerIndexOfField(field) });
77
+ };
79
78
 
80
- const { settingKey = "", step = 0, min = 0, type = "", options = [] } = fields[currentItem];
81
- if (!settingKey) return;
79
+ getFocusedField = () => {
80
+ const { currentArrowVerticalItem, fields = [] } = this.state;
82
81
 
83
- if (options?.length) {
84
- const newValue = (currentItemInner === 0 ? options.length : (currentItemInner ?? 0)) - 1;
82
+ if (currentArrowVerticalItem == null || !fields.length) return;
83
+
84
+ return fields[currentArrowVerticalItem];
85
+ };
86
+
87
+ innerIndexOfField = (field?: SettingsField) => {
88
+ const options = field?.options ?? [];
89
+ if (!options.length) return 0;
90
+
91
+ const settingKey = field?.settingKey;
92
+ if (!settingKey) return 0;
93
+
94
+ const index = options.findIndex((option) => option.value === this.shared.settings[settingKey]);
95
+
96
+ return index >= 0 ? index : 0;
97
+ };
98
+
99
+ optionIndexOf = (length: number, index = 0) => {
100
+ if (!length) return 0;
101
+
102
+ return ((index % length) + length) % length;
103
+ };
104
+
105
+ shiftOptions = (direction: -1 | 1) => {
106
+ const field = this.getFocusedField();
107
+ const options = field?.options ?? [];
108
+
109
+ if (!options.length) return;
110
+
111
+ const currentItemInner = this.optionIndexOf(options.length, (this.state.currentItemInner ?? 0) + direction);
112
+ const option = options[currentItemInner];
113
+ const settingKey = field?.settingKey;
114
+
115
+ void this.updateStateAndViewFast({ currentItemInner });
85
116
 
86
- return this.updateStateAndViewFast({ currentItemInner: newValue });
117
+ if (settingKey && option && !option.action) {
118
+ void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
87
119
  }
120
+ };
121
+
122
+ onArrowLeft = () => {
123
+ const field = this.getFocusedField();
124
+
125
+ if (!field) return;
126
+
127
+ if (field.options?.length) return this.shiftOptions(-1);
128
+
129
+ const { settingKey = "", step = 0, min = 0, type = "" } = field;
130
+ if (!settingKey) return;
88
131
 
89
132
  if (type === "toggle") {
90
133
  return this.shared.settings[settingKey] === true && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: false });
@@ -98,16 +141,14 @@ export class SettingsViewController extends ModuleController<
98
141
  };
99
142
 
100
143
  onArrowRight = () => {
101
- const { currentItemInner = 0, currentItem, fields = [] } = this.state;
144
+ const field = this.getFocusedField();
102
145
 
103
- if (currentItem == null || !fields.length) return;
146
+ if (!field) return;
104
147
 
105
- const { settingKey = "", step = 0, max = 0, type = "", options = [] } = fields[currentItem];
106
- if (!settingKey) return;
148
+ if (field.options?.length) return this.shiftOptions(1);
107
149
 
108
- if (options?.length) {
109
- return this.updateStateAndViewFast({ currentItemInner: ((currentItemInner ?? 0) + 1) % options.length });
110
- }
150
+ const { settingKey = "", step = 0, max = 0, type = "" } = field;
151
+ if (!settingKey) return;
111
152
 
112
153
  if (type === "toggle") {
113
154
  return this.shared.settings[settingKey] === false && void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: true });
@@ -121,17 +162,25 @@ export class SettingsViewController extends ModuleController<
121
162
  };
122
163
 
123
164
  onInteract = () => {
124
- const { currentItemInner = 0, currentItem, fields = [], locs = {} } = this.state;
165
+ const field = this.getFocusedField();
125
166
 
126
- if (currentItem == null || !fields.length) return;
167
+ if (!field) return;
127
168
 
128
- const { settingKey = "", type = "", options = [] } = fields[currentItem];
129
- if (!settingKey) return;
169
+ const { settingKey = "", type = "", options = [] } = field;
170
+ const { locs = {} } = this.state;
171
+
172
+ if (options?.length) {
173
+ const option = options[this.optionIndexOf(options.length, this.state.currentItemInner ?? 0)];
174
+
175
+ if (!option) return;
130
176
 
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 });
177
+ if (option.action) return this.PARAMS.SETTINGS_VIEW.ACTIONS[option.action](this, locs);
178
+ if (settingKey) return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: option.value });
179
+
180
+ return;
181
+ }
182
+
183
+ if (!settingKey) return;
135
184
 
136
185
  if (type === "toggle") return void this.emit(this.EVENTS.SETTINGS.SET, { name: settingKey, value: !this.shared.settings[settingKey] });
137
186
  };
@@ -140,8 +189,6 @@ export class SettingsViewController extends ModuleController<
140
189
  [this.CONST.CONTROLS.BUTTONS.INTERACT]: () => void this.onInteract(),
141
190
  [this.CONST.CONTROLS.BUTTONS.ARROW_LEFT]: () => void this.onArrowLeft(),
142
191
  [this.CONST.CONTROLS.BUTTONS.ARROW_RIGHT]: () => void this.onArrowRight(),
143
- [this.CONST.CONTROLS.BUTTONS.ARROW_UP]: this.decCurrentItem,
144
- [this.CONST.CONTROLS.BUTTONS.ARROW_BOTTOM]: this.incCurrentItem,
145
192
  };
146
193
  controlsCheckNext = true;
147
194
 
@@ -149,6 +196,8 @@ export class SettingsViewController extends ModuleController<
149
196
  this.on(this.EVENTS.SETTINGS_VIEW.SHOW, this.onShowFields);
150
197
  this.on(this.EVENTS.SETTINGS_VIEW.HIDE, this.onHide);
151
198
  this.on(this.EVENTS.SETTINGS_VIEW.TAB_SET, this.onTabSet);
199
+ this.on(this.EVENTS.SETTINGS_VIEW.STATE_UPDATE, this.onStateUpdate);
200
+ this.on(this.EVENTS.SETTINGS_VIEW.UPDATE_FAST, this.updateViewFast);
152
201
 
153
202
  this.on(this.EVENTS.SETTINGS.CHANGED, this.onSettingChange);
154
203
  this.on(this.EVENTS.COMPONENTS.EXTERNAL_UPDATE, this.onSettingChange);
@@ -0,0 +1,97 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+
3
+ import { spyEvent } from "@vnejs/test-utils";
4
+
5
+ import { SettingsViewArrows } from "../modules/arrows.js";
6
+ import { CONTROLS_EVENTS, createSettingsViewTestModule, registerSettingsViewPluginVne, SUBSCRIBE_EVENTS } from "./setup.js";
7
+
8
+ const generalTab = {
9
+ titleLocKey: "general",
10
+ fields: [{ settingKey: "a", type: "toggle" }, { settingKey: "b", type: "toggle" }],
11
+ };
12
+
13
+ describe("SettingsViewArrows", () => {
14
+ beforeEach(() => {
15
+ registerSettingsViewPluginVne();
16
+ });
17
+
18
+ it("SHOW pushes arrow controls above the controller layer", async () => {
19
+ const { module, observer } = createSettingsViewTestModule(SettingsViewArrows);
20
+ const arrows = module as SettingsViewArrows;
21
+ const push = spyEvent(observer, CONTROLS_EVENTS.PUSH);
22
+
23
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, { tab: generalTab });
24
+
25
+ expect(arrows.currentItem).toBeNull();
26
+ expect(arrows.fields).toHaveLength(2);
27
+ expect(push).toHaveBeenCalledExactlyOnceWith(
28
+ expect.objectContaining({
29
+ key: "settings.fields.controller.arrows",
30
+ checkNext: true,
31
+ index: arrows.PARAMS.SETTINGS_VIEW.ZINDEX + 1,
32
+ }),
33
+ );
34
+ });
35
+
36
+ it("HIDE pops arrow controls", async () => {
37
+ const { observer } = createSettingsViewTestModule(SettingsViewArrows);
38
+ const pop = spyEvent(observer, CONTROLS_EVENTS.POP);
39
+
40
+ await observer.emit(SUBSCRIBE_EVENTS.HIDE);
41
+
42
+ expect(pop).toHaveBeenCalledExactlyOnceWith({ key: "settings.fields.controller.arrows" });
43
+ });
44
+
45
+ it("first ARROW_BOTTOM selects 0 and emits STATE_UPDATE plus UPDATE_FAST", async () => {
46
+ const { module, observer } = createSettingsViewTestModule(SettingsViewArrows);
47
+ const arrows = module as SettingsViewArrows;
48
+ const stateUpdate = spyEvent(observer, SUBSCRIBE_EVENTS.STATE_UPDATE);
49
+ const updateFast = spyEvent(observer, SUBSCRIBE_EVENTS.UPDATE_FAST);
50
+
51
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, { tab: generalTab });
52
+ await arrows.incCurrentItem();
53
+
54
+ expect(arrows.currentItem).toBe(0);
55
+ expect(stateUpdate).toHaveBeenCalledExactlyOnceWith({ currentArrowVerticalItem: 0 });
56
+ expect(updateFast).toHaveBeenCalledOnce();
57
+ });
58
+
59
+ it("arrow wrap cycles fields", async () => {
60
+ const { module, observer } = createSettingsViewTestModule(SettingsViewArrows);
61
+ const arrows = module as SettingsViewArrows;
62
+ const stateUpdate = spyEvent(observer, SUBSCRIBE_EVENTS.STATE_UPDATE);
63
+
64
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, { tab: generalTab });
65
+ await arrows.incCurrentItem();
66
+ await arrows.incCurrentItem();
67
+ expect(arrows.currentItem).toBe(1);
68
+
69
+ await arrows.incCurrentItem();
70
+ expect(arrows.currentItem).toBe(0);
71
+ expect(stateUpdate).toHaveBeenLastCalledWith({ currentArrowVerticalItem: 0 });
72
+
73
+ await arrows.decCurrentItem();
74
+ expect(arrows.currentItem).toBe(1);
75
+ });
76
+
77
+ it("TAB_SET resets currentItem and syncs fields", async () => {
78
+ const { module, observer } = createSettingsViewTestModule(SettingsViewArrows);
79
+ const arrows = module as SettingsViewArrows;
80
+ const stateUpdate = spyEvent(observer, SUBSCRIBE_EVENTS.STATE_UPDATE);
81
+
82
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, { tab: generalTab });
83
+ await arrows.incCurrentItem();
84
+ expect(arrows.currentItem).toBe(0);
85
+
86
+ await observer.emit(SUBSCRIBE_EVENTS.TAB_SET, {
87
+ tab: {
88
+ titleLocKey: "audio",
89
+ fields: [{ settingKey: "c", type: "toggle" }, { settingKey: "d", type: "toggle" }, { settingKey: "e", type: "toggle" }],
90
+ },
91
+ });
92
+
93
+ expect(arrows.currentItem).toBeNull();
94
+ expect(arrows.fields).toHaveLength(3);
95
+ expect(stateUpdate).toHaveBeenLastCalledWith({ currentArrowVerticalItem: null });
96
+ });
97
+ });
@@ -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";
@@ -11,7 +11,7 @@ describe("SettingsViewController", () => {
11
11
  registerSettingsViewPluginVne();
12
12
  });
13
13
 
14
- it("SHOW stores fields from tab and maxCurrentItem", async () => {
14
+ it("SHOW stores fields from tab", async () => {
15
15
  const { module, observer } = createSettingsViewTestModule(SettingsViewController);
16
16
 
17
17
  await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
@@ -26,7 +26,6 @@ describe("SettingsViewController", () => {
26
26
  });
27
27
 
28
28
  expect((module as SettingsViewController).state.fields).toHaveLength(2);
29
- expect((module as SettingsViewController).maxCurrentItem).toBe(1);
30
29
  expect((module as SettingsViewController).state.isShow).toBe(true);
31
30
  expect((module as SettingsViewController).state.tabKey).toBe("general");
32
31
  expect(Object.keys((module as SettingsViewController).state.tabsContent ?? {})).toEqual(["general"]);
@@ -41,8 +40,7 @@ describe("SettingsViewController", () => {
41
40
  tab: { titleLocKey: "general", fields: [{ settingKey: "test.toggle", type: "toggle" }] },
42
41
  });
43
42
 
44
- (module as SettingsViewController).currentItem = 0;
45
- (module as SettingsViewController).updateState({ currentItem: 0 });
43
+ (module as SettingsViewController).updateState({ currentArrowVerticalItem: 0 });
46
44
 
47
45
  const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
48
46
 
@@ -85,9 +83,151 @@ describe("SettingsViewController", () => {
85
83
 
86
84
  expect((module as SettingsViewController).state.fields).toHaveLength(2);
87
85
  expect((module as SettingsViewController).state.showTextExample).toBe(true);
88
- expect((module as SettingsViewController).maxCurrentItem).toBe(1);
89
86
  expect((module as SettingsViewController).state.currentItemInner).toBe(0);
87
+ expect((module as SettingsViewController).state.currentArrowVerticalItem).toBeNull();
90
88
  expect((module as SettingsViewController).state.tabKey).toBe("audio");
91
89
  expect(Object.keys((module as SettingsViewController).state.tabsContent ?? {}).sort()).toEqual(["audio", "general"]);
92
90
  });
91
+
92
+ it("ARROW_RIGHT on selector_texts cycles inner and SET", async () => {
93
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController, {
94
+ shared: { settings: { "test.lang": "ru" } },
95
+ });
96
+
97
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
98
+ tab: {
99
+ titleLocKey: "general",
100
+ fields: [
101
+ {
102
+ settingKey: "test.lang",
103
+ type: "selector_texts",
104
+ options: [
105
+ { text: "ru", value: "ru" },
106
+ { text: "en", value: "en" },
107
+ { text: "de", value: "de" },
108
+ ],
109
+ },
110
+ ],
111
+ },
112
+ });
113
+
114
+ const controller = module as SettingsViewController;
115
+
116
+ controller.updateState({ currentArrowVerticalItem: 0, currentItemInner: 0 });
117
+
118
+ const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
119
+
120
+ controller.onArrowRight();
121
+
122
+ expect(controller.state.currentItemInner).toBe(1);
123
+ expect(settingsSet).toHaveBeenCalledExactlyOnceWith({ name: "test.lang", value: "en" });
124
+ });
125
+
126
+ it("ARROW_LEFT on selector_texts wraps inner and SET", async () => {
127
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController, {
128
+ shared: { settings: { "test.lang": "ru" } },
129
+ });
130
+
131
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
132
+ tab: {
133
+ titleLocKey: "general",
134
+ fields: [
135
+ {
136
+ settingKey: "test.lang",
137
+ type: "selector_texts",
138
+ options: [
139
+ { text: "ru", value: "ru" },
140
+ { text: "en", value: "en" },
141
+ { text: "de", value: "de" },
142
+ ],
143
+ },
144
+ ],
145
+ },
146
+ });
147
+
148
+ const controller = module as SettingsViewController;
149
+
150
+ controller.updateState({ currentArrowVerticalItem: 0, currentItemInner: 0 });
151
+
152
+ const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
153
+
154
+ controller.onArrowLeft();
155
+
156
+ expect(controller.state.currentItemInner).toBe(2);
157
+ expect(settingsSet).toHaveBeenCalledExactlyOnceWith({ name: "test.lang", value: "de" });
158
+ });
159
+
160
+ it("ARROW_* on actions without settingKey only moves inner", async () => {
161
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController);
162
+ const controller = module as SettingsViewController;
163
+ const doA = vi.fn();
164
+ const doB = vi.fn();
165
+
166
+ controller.PARAMS.SETTINGS_VIEW.ACTIONS.do_a = doA;
167
+ controller.PARAMS.SETTINGS_VIEW.ACTIONS.do_b = doB;
168
+
169
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
170
+ tab: {
171
+ titleLocKey: "general",
172
+ fields: [
173
+ {
174
+ type: "actions",
175
+ options: [
176
+ { locKey: "a", action: "do_a" },
177
+ { locKey: "b", action: "do_b" },
178
+ ],
179
+ },
180
+ ],
181
+ },
182
+ });
183
+
184
+ controller.updateState({ currentArrowVerticalItem: 0, currentItemInner: 0 });
185
+
186
+ const settingsSet = spyEvent(observer, SETTINGS_EVENTS.SET);
187
+
188
+ controller.onArrowLeft();
189
+
190
+ expect(controller.state.currentItemInner).toBe(1);
191
+ expect(doA).not.toHaveBeenCalled();
192
+ expect(doB).not.toHaveBeenCalled();
193
+ expect(settingsSet).not.toHaveBeenCalled();
194
+
195
+ controller.onInteract();
196
+
197
+ expect(doB).toHaveBeenCalledOnce();
198
+ expect(doA).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it("STATE_UPDATE syncs currentItemInner to the current option", async () => {
202
+ const { module, observer } = createSettingsViewTestModule(SettingsViewController, {
203
+ shared: { settings: { "test.lang": "en" } },
204
+ });
205
+
206
+ await observer.emit(SUBSCRIBE_EVENTS.SHOW, {
207
+ tab: {
208
+ titleLocKey: "general",
209
+ fields: [
210
+ { settingKey: "a", type: "toggle" },
211
+ {
212
+ settingKey: "test.lang",
213
+ type: "selector_texts",
214
+ options: [
215
+ { text: "ru", value: "ru" },
216
+ { text: "en", value: "en" },
217
+ { text: "de", value: "de" },
218
+ ],
219
+ },
220
+ ],
221
+ },
222
+ });
223
+
224
+ const controller = module as SettingsViewController;
225
+
226
+ controller.updateState({ currentArrowVerticalItem: 0, currentItemInner: 99 });
227
+
228
+ await observer.emit(SUBSCRIBE_EVENTS.STATE_UPDATE, { currentArrowVerticalItem: 1 });
229
+
230
+ expect(controller.state.currentArrowVerticalItem).toBe(1);
231
+ expect(controller.state.currentItemInner).toBe(1);
232
+ });
93
233
  });
@@ -23,7 +23,7 @@ export const createSettingsViewTestModule = <T extends Parameters<typeof baseCre
23
23
  const result = baseCreateTestModule(ModuleClass, {
24
24
  ...rest,
25
25
  state: {
26
- "settings.fields.controller": { isShow: false, fields: [], currentItem: null, currentItemInner: 0 },
26
+ "settings.fields.controller": { isShow: false, fields: [], currentArrowVerticalItem: null, currentItemInner: 0 },
27
27
  ...(stateOption ?? {}),
28
28
  },
29
29
  shared: {
@@ -36,9 +36,11 @@ export const createSettingsViewTestModule = <T extends Parameters<typeof baseCre
36
36
  stubSilentEvent(result.observer, SUBSCRIBE_EVENTS.SHOW);
37
37
  stubSilentEvent(result.observer, SUBSCRIBE_EVENTS.HIDE);
38
38
  stubSilentEvent(result.observer, SUBSCRIBE_EVENTS.UPDATE);
39
+ stubSilentEvent(result.observer, SUBSCRIBE_EVENTS.UPDATE_FAST);
40
+ stubSilentEvent(result.observer, SUBSCRIBE_EVENTS.STATE_UPDATE);
39
41
  stubSilentEvent(result.observer, SUBSCRIBE_EVENTS.TAB_SET);
40
42
 
41
43
  return result;
42
44
  };
43
45
 
44
- export { SUBSCRIBE_EVENTS, PARAMS, PLUGIN_NAME };
46
+ export { SUBSCRIBE_EVENTS, PARAMS, PLUGIN_NAME, CONTROLS_EVENTS };
@@ -15,7 +15,7 @@ export type SettingsViewPluginState = {
15
15
  fields?: SettingsField[];
16
16
  locs?: Record<string, string>;
17
17
  showTextExample?: boolean;
18
- currentItem?: number | null;
18
+ currentArrowVerticalItem?: number | null;
19
19
  currentItemInner?: number | null;
20
20
  };
21
21
 
@@ -21,7 +21,7 @@ const SettingsViewComponent = ({ store, onMount, PARAMS, emit, EVENTS, shared, C
21
21
  locs = {},
22
22
  tabsContent = {},
23
23
  tabKey = "",
24
- currentItem = null,
24
+ currentArrowVerticalItem = null,
25
25
  currentItemInner = null,
26
26
  } = useStoreState<SettingsViewPluginState>(store, onMount);
27
27
 
@@ -40,8 +40,8 @@ const SettingsViewComponent = ({ store, onMount, PARAMS, emit, EVENTS, shared, C
40
40
  );
41
41
 
42
42
  const propsTab = useMemo(
43
- () => ({ store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isForce: isRealForce, currentItem, currentItemInner, locs }),
44
- [store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isRealForce, currentItem, currentItemInner, locs],
43
+ () => ({ store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isForce: isRealForce, currentItem: currentArrowVerticalItem, currentItemInner, locs }),
44
+ [store, onMount, PARAMS, emit, EVENTS, shared, CONST, SETTINGS, isRealForce, currentArrowVerticalItem, currentItemInner, locs],
45
45
  );
46
46
 
47
47
  return (