Skip to content

Commit cbf9b8a

Browse files
committed
Add Controls combo framework and editable Account user controls - PR_26162_042-controls-combo-framework-and-user-control-editing
1 parent ef56c9d commit cbf9b8a

10 files changed

Lines changed: 984 additions & 2403 deletions

account/user-controls-page.js

Lines changed: 118 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ export class AccountUserControlsPage {
130130
defaultLists: [...root.querySelectorAll("[data-account-user-controls-default-list]")],
131131
deviceSelect: root.querySelector("[data-account-user-controls-device]"),
132132
deviceStatus: root.querySelector("[data-account-user-controls-device-status]"),
133-
list: root.querySelector("[data-account-user-controls-list]"),
133+
familyButtons: [...root.querySelectorAll("[data-account-user-controls-edit-family]")],
134+
lists: [...root.querySelectorAll("[data-account-user-controls-list]")],
134135
refresh: root.querySelector("[data-account-user-controls-refresh]"),
135136
saveAll: root.querySelector("[data-account-user-controls-save-all]"),
136137
status: root.querySelector("[data-account-user-controls-status]"),
@@ -152,10 +153,15 @@ export class AccountUserControlsPage {
152153
this.elements.addProfile?.addEventListener("click", () => this.addProfileForSelectedDevice());
153154
this.elements.saveAll?.addEventListener("click", () => this.saveCurrentState());
154155
this.elements.deviceSelect?.addEventListener("change", () => this.renderDeviceStatus());
155-
this.elements.list?.addEventListener("click", (event) => this.handleListClick(event));
156-
this.elements.list?.addEventListener("change", (event) => this.handleListChange(event));
157-
this.elements.list?.addEventListener("input", (event) => this.handleListInput(event));
158-
this.elements.list?.addEventListener("dblclick", (event) => this.handleListDoubleClick(event));
156+
this.elements.familyButtons.forEach((button) => {
157+
button.addEventListener("click", () => this.editFamilyMappings(button.dataset.accountUserControlsEditFamily || ""));
158+
});
159+
this.elements.lists.forEach((list) => {
160+
list.addEventListener("click", (event) => this.handleListClick(event));
161+
list.addEventListener("change", (event) => this.handleListChange(event));
162+
list.addEventListener("input", (event) => this.handleListInput(event));
163+
list.addEventListener("dblclick", (event) => this.handleListDoubleClick(event));
164+
});
159165
this.startDevicePolling();
160166
}
161167

@@ -177,25 +183,7 @@ export class AccountUserControlsPage {
177183
}
178184

179185
deviceOptions() {
180-
const keyboard = {
181-
controllerId: "generic-keyboard",
182-
controllerName: "Keyboard",
183-
deviceType: "Keyboard",
184-
inputs: [...KEYBOARD_INPUTS],
185-
label: "Keyboard",
186-
mappingProfile: "Keyboard Profile",
187-
value: "keyboard",
188-
};
189-
const mouse = {
190-
controllerId: "generic-mouse",
191-
controllerName: "Mouse",
192-
deviceType: "Mouse",
193-
inputs: [...MOUSE_INPUTS],
194-
label: "Mouse",
195-
mappingProfile: "Mouse Profile",
196-
value: "mouse",
197-
};
198-
const gamepads = this.availableGamepads().map((gamepad) => {
186+
return this.availableGamepads().map((gamepad) => {
199187
const controllerName = gamepad.id || `Gamepad ${gamepad.index}`;
200188
return {
201189
controllerId: gamepad.id || `gamepad-${gamepad.index}`,
@@ -207,15 +195,37 @@ export class AccountUserControlsPage {
207195
value: `gamepad-${gamepad.index}`,
208196
};
209197
});
210-
return [keyboard, mouse, ...gamepads];
198+
}
199+
200+
familyDevice(family) {
201+
if (family === "Mouse") {
202+
return {
203+
controllerId: "generic-mouse",
204+
controllerName: "Mouse",
205+
deviceType: "Mouse",
206+
inputs: [...MOUSE_INPUTS],
207+
label: "Mouse",
208+
mappingProfile: "Mouse Profile",
209+
value: "mouse",
210+
};
211+
}
212+
return {
213+
controllerId: "generic-keyboard",
214+
controllerName: "Keyboard",
215+
deviceType: "Keyboard",
216+
inputs: [...KEYBOARD_INPUTS],
217+
label: "Keyboard",
218+
mappingProfile: "Keyboard Profile",
219+
value: "keyboard",
220+
};
211221
}
212222

213223
deviceRefreshMessage() {
214224
const gamepadCount = this.availableGamepads().length;
215225
if (gamepadCount > 0) {
216-
return `PASS: Keyboard and Mouse available. ${gamepadCount} game controller${gamepadCount === 1 ? "" : "s"} detected automatically.`;
226+
return `PASS: ${gamepadCount} game controller${gamepadCount === 1 ? "" : "s"} detected automatically.`;
217227
}
218-
return "PASS: Keyboard and Mouse available. Gamepads auto-detect after browser exposes them.";
228+
return "PASS: Game controllers auto-detect after the browser exposes them.";
219229
}
220230

221231
selectedDevice() {
@@ -287,7 +297,7 @@ export class AccountUserControlsPage {
287297
}
288298
const selectedValue = this.elements.deviceSelect.value;
289299
const options = [
290-
{ label: "Choose a physical controller", value: "" },
300+
{ label: "Choose a game controller", value: "" },
291301
...this.deviceOptions().map((device) => ({ label: device.label, value: device.value })),
292302
];
293303
this.elements.deviceSelect.classList.add("tool-form-control");
@@ -368,30 +378,62 @@ export class AccountUserControlsPage {
368378
}, { once: true });
369379
}
370380

381+
profileListFamily(profile) {
382+
const deviceType = normalizeText(profile?.deviceType);
383+
if (deviceType === "Keyboard" || deviceType === "Mouse") {
384+
return deviceType;
385+
}
386+
return "Gamepad";
387+
}
388+
389+
emptyProfileMessage(family) {
390+
if (family === "Keyboard") {
391+
return "No keyboard user control profile saved yet.";
392+
}
393+
if (family === "Mouse") {
394+
return "No mouse user control profile saved yet.";
395+
}
396+
return "No game controller profiles saved yet.";
397+
}
398+
371399
renderProfiles() {
372-
if (!this.elements.list) {
400+
if (!this.elements.lists.length) {
373401
return;
374402
}
375-
const rows = [];
403+
const rowsByFamily = new Map(this.elements.lists.map((list) => [
404+
list.dataset.accountUserControlsListFamily || "Gamepad",
405+
[],
406+
]));
376407
if (this.editingProfile) {
377-
rows.push(...this.renderEditingRows(this.editingProfile.values));
408+
const family = this.profileListFamily(this.editingProfile.values);
409+
rowsByFamily.get(family)?.push(...this.renderEditingRows(this.editingProfile.values));
378410
}
379411
const visibleProfiles = this.editingProfile?.id
380412
? this.profiles.filter((profile) => profile.id !== this.editingProfile.id)
381413
: this.profiles;
382-
rows.push(...visibleProfiles.map((profile) => this.renderProfileRow(profile)));
383-
if (!rows.length) {
384-
const row = document.createElement("tr");
385-
const cell = document.createElement("td");
386-
cell.colSpan = 7;
387-
cell.textContent = "No account user control profiles saved yet.";
388-
row.append(cell);
389-
rows.push(row);
390-
}
391-
this.elements.list.replaceChildren(...rows);
414+
visibleProfiles.forEach((profile) => {
415+
const family = this.profileListFamily(profile);
416+
rowsByFamily.get(family)?.push(this.renderProfileRow(profile));
417+
});
418+
this.elements.lists.forEach((list) => {
419+
const family = list.dataset.accountUserControlsListFamily || "Gamepad";
420+
const rows = rowsByFamily.get(family) || [];
421+
if (!rows.length) {
422+
const row = document.createElement("tr");
423+
const cell = document.createElement("td");
424+
cell.colSpan = 7;
425+
cell.textContent = this.emptyProfileMessage(family);
426+
row.append(cell);
427+
rows.push(row);
428+
}
429+
list.replaceChildren(...rows);
430+
});
392431
if (this.elements.addProfile) {
393432
this.elements.addProfile.disabled = Boolean(this.editingProfile);
394433
}
434+
this.elements.familyButtons.forEach((button) => {
435+
button.disabled = Boolean(this.editingProfile);
436+
});
395437
}
396438

397439
profileInputSummary(profile) {
@@ -455,8 +497,18 @@ export class AccountUserControlsPage {
455497
const wrapper = document.createElement("div");
456498
wrapper.className = "content-grid";
457499
wrapper.dataset.accountUserControlsInputPair = "true";
458-
const label = document.createElement("strong");
459-
label.textContent = inputMapping.physicalInput;
500+
const editablePhysicalInput = profile.deviceType === "Keyboard" || profile.deviceType === "Mouse";
501+
const physicalInputControl = editablePhysicalInput
502+
? document.createElement("input")
503+
: document.createElement("strong");
504+
if (editablePhysicalInput) {
505+
physicalInputControl.type = "text";
506+
physicalInputControl.value = inputMapping.physicalInput;
507+
physicalInputControl.dataset.accountUserControlsPhysicalInput = String(index);
508+
physicalInputControl.setAttribute("aria-label", `${profile.deviceType} physical input`);
509+
} else {
510+
physicalInputControl.textContent = inputMapping.physicalInput;
511+
}
460512
const stack = document.createElement("div");
461513
stack.className = "content-stack content-stack--compact";
462514
const normalizedOptions = [
@@ -522,7 +574,7 @@ export class AccountUserControlsPage {
522574
validation.className = "status";
523575
validation.dataset.accountUserControlsInputValidation = String(index);
524576
stack.append(validation);
525-
wrapper.append(label, stack);
577+
wrapper.append(physicalInputControl, stack);
526578
return wrapper;
527579
}
528580

@@ -579,24 +631,42 @@ export class AccountUserControlsPage {
579631
this.setStatus(`Review ${profile.mappingProfile} before saving.`);
580632
}
581633

634+
editFamilyMappings(family) {
635+
const normalizedFamily = family === "Mouse" ? "Mouse" : "Keyboard";
636+
const device = this.familyDevice(normalizedFamily);
637+
const existing = this.profiles.find((profile) => profile.deviceType === device.deviceType && profile.controllerId === device.controllerId);
638+
if (existing) {
639+
this.editingProfile = { id: existing.id, values: existing };
640+
this.renderProfiles();
641+
this.setStatus(`Editing existing ${existing.mappingProfile}.`);
642+
return;
643+
}
644+
const profile = this.profileFromDevice(device);
645+
this.editingProfile = { id: profile.id, values: profile };
646+
this.renderProfiles();
647+
this.setStatus(`Review ${profile.mappingProfile} before saving.`);
648+
}
649+
582650
profileFromEditingRow() {
583651
const values = this.editingProfile?.values || {};
584-
const details = this.elements.list?.querySelector("[data-account-user-controls-editing-details]");
652+
const details = this.root.querySelector("[data-account-user-controls-editing-details]");
585653
const inputMappings = (values.inputMappings || []).map((mapping, index) => {
654+
const physicalInputControl = details?.querySelector(`[data-account-user-controls-physical-input="${index}"]`);
586655
const normalizedSelect = details?.querySelector(`[data-account-user-controls-input-normalized="${index}"]`);
587656
const negativeSelect = details?.querySelector(`[data-account-user-controls-input-negative="${index}"]`);
588657
const positiveSelect = details?.querySelector(`[data-account-user-controls-input-positive="${index}"]`);
589658
const deadzoneInput = details?.querySelector(`[data-account-user-controls-deadzone="${index}"]`);
590659
const invertInput = details?.querySelector(`[data-account-user-controls-invert="${index}"]`);
591660
const sensitivityInput = details?.querySelector(`[data-account-user-controls-sensitivity="${index}"]`);
661+
const physicalInput = normalizeText(physicalInputControl?.value ?? mapping.physicalInput);
592662
const negativeNormalizedInput = normalizeText(negativeSelect?.value ?? mapping.negativeNormalizedInput);
593663
const positiveNormalizedInput = normalizeText(positiveSelect?.value ?? mapping.positiveNormalizedInput);
594664
return {
595665
deadzone: Number(deadzoneInput?.value ?? mapping.deadzone),
596666
invert: Boolean(invertInput?.checked ?? mapping.invert),
597667
negativeNormalizedInput,
598668
normalizedInput: positiveNormalizedInput || normalizeText(normalizedSelect?.value ?? mapping.normalizedInput) || negativeNormalizedInput,
599-
physicalInput: mapping.physicalInput,
669+
physicalInput,
600670
positiveNormalizedInput,
601671
sensitivity: sensitivityInput ? Number(sensitivityInput.value) : mapping.sensitivity,
602672
};
@@ -605,6 +675,7 @@ export class AccountUserControlsPage {
605675
...values,
606676
id: this.editingProfile?.id || "",
607677
inputMappings,
678+
inputs: inputMappings.map((mapping) => mapping.physicalInput),
608679
});
609680
}
610681

@@ -662,7 +733,7 @@ export class AccountUserControlsPage {
662733

663734
renderInputValidation(validation) {
664735
const invalidIndexes = new Set(validation.invalidIndexes || []);
665-
this.elements.list?.querySelectorAll("[data-account-user-controls-input-validation]").forEach((status) => {
736+
this.root.querySelectorAll("[data-account-user-controls-input-validation]").forEach((status) => {
666737
const index = Number(status.dataset.accountUserControlsInputValidation);
667738
status.textContent = invalidIndexes.has(index)
668739
? "Needs normalized control, valid deadzone, and valid sensitivity."

account/user-controls.html

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ <h2>Physical Input Mapping</h2>
4747
<tbody data-account-user-controls-default-list data-account-user-controls-default-family="Keyboard"></tbody>
4848
</table>
4949
</div>
50+
<div class="content-cluster" aria-label="Keyboard user controls">
51+
<button class="btn" type="button" data-account-user-controls-edit-family="Keyboard">Edit Keyboard Mappings</button>
52+
</div>
53+
<div class="table-wrapper">
54+
<table class="data-table" aria-label="Keyboard user control profile" data-account-user-controls-table="Keyboard">
55+
<thead>
56+
<tr>
57+
<th>Physical Controller</th>
58+
<th>Physical Input</th>
59+
<th>Normalized Control</th>
60+
<th>Deadzone</th>
61+
<th>Invert</th>
62+
<th>Sensitivity</th>
63+
<th>Actions</th>
64+
</tr>
65+
</thead>
66+
<tbody data-account-user-controls-list data-account-user-controls-list-family="Keyboard"></tbody>
67+
</table>
68+
</div>
5069
</div>
5170
</details>
5271
<details class="vertical-accordion" open data-account-user-controls-section="Mouse">
@@ -65,6 +84,25 @@ <h2>Physical Input Mapping</h2>
6584
<tbody data-account-user-controls-default-list data-account-user-controls-default-family="Mouse"></tbody>
6685
</table>
6786
</div>
87+
<div class="content-cluster" aria-label="Mouse user controls">
88+
<button class="btn" type="button" data-account-user-controls-edit-family="Mouse">Edit Mouse Mappings</button>
89+
</div>
90+
<div class="table-wrapper">
91+
<table class="data-table" aria-label="Mouse user control profile" data-account-user-controls-table="Mouse">
92+
<thead>
93+
<tr>
94+
<th>Physical Controller</th>
95+
<th>Physical Input</th>
96+
<th>Normalized Control</th>
97+
<th>Deadzone</th>
98+
<th>Invert</th>
99+
<th>Sensitivity</th>
100+
<th>Actions</th>
101+
</tr>
102+
</thead>
103+
<tbody data-account-user-controls-list data-account-user-controls-list-family="Mouse"></tbody>
104+
</table>
105+
</div>
68106
</div>
69107
</details>
70108
<details class="vertical-accordion" open data-account-user-controls-section="Game Controllers">
@@ -73,13 +111,13 @@ <h2>Physical Input Mapping</h2>
73111
<div class="content-cluster" aria-label="Account controller device selection">
74112
<button class="btn" type="button" data-account-user-controls-refresh>Refresh Devices</button>
75113
<label>
76-
Physical Controller
114+
Game Controller
77115
<select data-account-user-controls-device></select>
78116
</label>
79117
<button class="btn" type="button" data-account-user-controls-add-profile>Create User Control Profile</button>
80118
<button class="btn primary" type="button" data-account-user-controls-save-all>Save</button>
81119
</div>
82-
<p class="status" role="status" data-account-user-controls-device-status>Keyboard and Mouse are available. Game controllers auto-detect after the browser exposes them.</p>
120+
<p class="status" role="status" data-account-user-controls-device-status>Game controllers auto-detect after the browser exposes them.</p>
83121
<div class="table-wrapper">
84122
<table class="data-table" aria-label="Account user control profiles" data-account-user-controls-table>
85123
<thead>
@@ -93,7 +131,7 @@ <h2>Physical Input Mapping</h2>
93131
<th>Actions</th>
94132
</tr>
95133
</thead>
96-
<tbody data-account-user-controls-list></tbody>
134+
<tbody data-account-user-controls-list data-account-user-controls-list-family="Gamepad"></tbody>
97135
</table>
98136
</div>
99137
</div>

0 commit comments

Comments
 (0)