ENH: Add a focus grid for filtering crafting items by group

This commit is contained in:
bananarama92 2025-03-27 17:55:47 +01:00
parent ac70b2df37
commit d66c818d7e
No known key found for this signature in database
GPG key ID: E83C7D3B5DA36248
3 changed files with 138 additions and 42 deletions
BondageClub

View file

@ -132,6 +132,11 @@
height: calc(2.5 * var(--button-size));
}
#crafting-asset-grid button[data-unload]:not([aria-checked="true"]),
#crafting-asset-grid button[data-unload-group]:not([aria-checked="true"]) {
display: none;
}
#crafting-property-grid button {
width: calc(5.7 * var(--button-size));
height: calc(1.5 * var(--button-size));

View file

@ -748,10 +748,6 @@ var CraftingEventListeners = {
return;
}
// Trigger a search query in order to filter the results by whatever input the user has specified
const searchInput = sidePannel.querySelector("input[type='search']");
searchInput?.dispatchEvent(new Event("input"));
if (this.getAttribute("aria-checked") === "true") {
controlButton.innerHTML = this.innerHTML;
return;
@ -780,8 +776,8 @@ var CraftingEventListeners = {
searchResultCandidates?.querySelectorAll("button.button").forEach(button => {
const label = button.querySelector(".button-label");
if (label) {
const displayStyle = (button.getAttribute("aria-checked") === "true" || label.textContent.toUpperCase().includes(query)) ? "" : "none";
/** @type {HTMLButtonElement} */(button).style.display = displayStyle;
const displayStyle = (button.getAttribute("aria-checked") === "true" || label.textContent.toUpperCase().includes(query)) ? false : true;
button.toggleAttribute("data-unload", displayStyle);
}
});
},
@ -808,21 +804,92 @@ var CraftingEventListeners = {
}
descriptionInput.dispatchEvent(new InputEvent("input"));
},
/**
* @private
* @type {(this: HTMLButtonElement, ev: MouseEvent) => void}
*/
_ClickGroup: function _ClickGroup(ev) {
const groupName = /** @type {AssetGroupItemName} */(this.name);
const assetList = document.getElementById(CraftingID.assetGrid);
if (!assetList) {
ev.stopImmediatePropagation();
return;
}
if (this.getAttribute("aria-checked") === "true") {
// Apply the filtering
for (const button of assetList.children) {
button.toggleAttribute("data-unload-group", button.getAttribute("data-group") !== groupName);
}
// Update the label of the asset panel
document.querySelector(`#${CraftingID.assetHeader} > span`)?.replaceChildren(
`${TextGet("SelectItem")} ${TextGet("SelectItemSuffix")} ${this.getAttribute("aria-label").toLocaleLowerCase()}`,
);
// Make sure that the asset panel is open and scroll to the top
document.querySelector(`#${CraftingID.assetButton}[aria-checked="false"]`)?.dispatchEvent(new MouseEvent("click"));
assetList.scrollTo({ top: 0 });
} else {
document.querySelector(`#${CraftingID.assetHeader} > span`)?.replaceChildren(TextGet("SelectItem"));
for (const button of assetList.children) {
button.toggleAttribute("data-unload-group", false);
}
const checked = assetList.querySelector("[aria-checked='true']");
if (checked) {
checked.scrollIntoView({ behavior: "instant" });
} else {
assetList.scrollTo({ top: 0 });
}
}
},
/**
* @private
* @type {(this: HTMLInputElement, ev: FocusEvent) => Promise<void>}
*/
_FocusSearchAsset: async function _FocusSearchAsset(ev) {
const focusGrid = document.getElementById(CraftingID.centerPanel);
if (!focusGrid) {
ev.stopImmediatePropagation();
return;
}
const group = /** @type {"ALL" | AssetGroupItemName} */(focusGrid.querySelector("[role='radio'][aria-checked='true']")?.getAttribute("name") ?? "ALL");
const cachedGroup = this.getAttribute("data-group");
if (cachedGroup === group) {
return;
}
let options = CraftingElements._SearchCache.get(group);
if (!options) {
const searchResults = document.getElementById(this.getAttribute("aria-controls"));
const query = group === "ALL" ? ".button-label" : `[data-group='${group}'] .button-label`;
options = Array.from(searchResults?.querySelectorAll(query) ?? []).map(e => ElementCreate({ tag: "option", attributes: { value: e.textContent }}));
CraftingElements._SearchCache.set(group, options);
}
this.list?.replaceChildren(...options);
this.setAttribute("data-group", group);
},
/**
* @private
* @type {(this: HTMLInputElement, ev: FocusEvent) => Promise<void>}
*/
_FocusSearch: async function _FocusSearch(ev) {
if (this.list?.options.length) {
return;
}
const searchResults = document.getElementById(this.getAttribute("aria-controls"));
const options = Array.from(searchResults?.querySelectorAll(".button-label") ?? []).map(e => ElementCreate({ tag: "option", attributes: { value: e.textContent }}));
this.list?.replaceChildren(...options);
},
};
var CraftingElements = {
/**
* @private
* @param {string} controls
* @returns {() => string[]}
*/
_SearchInputGetDataList: function _SearchInputGetDataList(controls) {
return () => {
const searchResults = document.getElementById(controls);
return Array.from(searchResults?.querySelectorAll("button > label") ?? []).map(e => e.textContent);
};
},
/**
* @private
* @param {string} id
@ -830,15 +897,35 @@ var CraftingElements = {
* @param {string} placeholder
* @returns {HTMLInputElement}
*/
_SearchInput: function _SearchInput(id, controls, placeholder) {
const ret = ElementCreateSearchInput(id, CraftingElements._SearchInputGetDataList(controls));
ret.setAttribute("aria-controls", controls);
ret.setAttribute("size", 0);
ret.addEventListener("input", CraftingEventListeners._InputSearch);
ret.placeholder = placeholder;
return ret;
_SearchInput: function _SearchInput(id, controls, placeholder, assetSearch=false) {
return ElementCreate({
tag: "input",
attributes: {
type: "search",
id,
placeholder,
list: `${id}-datalist`,
size: 0,
"aria-controls": controls,
},
dataAttributes: {
group: undefined, // Initialized and managed by the `focus` event listener for asset searches
},
eventListeners: {
input: CraftingEventListeners._InputSearch,
focus: assetSearch ? CraftingEventListeners._FocusSearchAsset : CraftingEventListeners._FocusSearch,
},
children: [
{ tag: "datalist", attributes: { id: `${id}-datalist` } },
],
});
},
/**
* @type {Map<"ALL" | AssetGroupItemName, readonly HTMLOptionElement[]>}
*/
_SearchCache: new Map(),
/**
* @private
* @param {string} id
@ -941,6 +1028,11 @@ function CraftingLoad() {
),
ElementButton.Create(CraftingID.uploadButton, CraftingEventListeners._ClickUpload, { tooltip: TextGet("Upload") }),
ElementButton.Create(CraftingID.downloadButton, CraftingEventListeners._ClickDownload, { tooltip: TextGet("Download") }),
ElementButton.Create(
CraftingID.undressButton, CraftingEventListeners._ClickUndress,
{ tooltip: TextGet("Undress"), role: "menuitemcheckbox" },
{ button: { attributes: { "aria-checked": CraftingNakedPreview ? "true" : "false" } } },
),
],
{ direction: "rtl" },
),
@ -1019,7 +1111,7 @@ function CraftingLoad() {
attributes: { id: CraftingID.assetHeader },
children: [
{ tag: "span", children: [TextGet("SelectItem")] },
CraftingElements._SearchInput(CraftingID.assetSearch, CraftingID.assetGrid, TextGet("FilterAsset")),
CraftingElements._SearchInput(CraftingID.assetSearch, CraftingID.assetGrid, TextGet("FilterAsset"), true),
],
},
{
@ -1076,14 +1168,7 @@ function CraftingLoad() {
{ menu: { attributes: { "aria-orientation": "vertical" }, parent } },
),
ElementCreate({
tag: "div",
attributes: { id: CraftingID.centerPanel },
parent,
children: [
ElementButton.Create(CraftingID.undressButton, CraftingEventListeners._ClickUndress),
],
});
parent.append(DialogFocusGroup.Create(CraftingID.centerPanel, CraftingEventListeners._ClickGroup, { useDynamicGroupName: true }));
ElementCreate({
tag: "div",
@ -1920,12 +2005,13 @@ function CraftingExitResetElements() {
// Clear all search inputs and undo their filtering
const searchInputs = /** @type {NodeListOf<HTMLInputElement>} */(document.querySelectorAll(`#${CraftingID.leftPanel} input[type='search']`));
searchInputs.forEach((searchInp) => {
if (searchInp.value) {
searchInp.value = "";
searchInp.dispatchEvent(new Event("input"));
}
});
searchInputs.forEach((searchInp) => searchInp.value ||= "");
const focusGroup = document.querySelector(`#${CraftingID.centerPanel} [role='radio'][aria-checked='true']`);
if (focusGroup) {
focusGroup.dispatchEvent(new MouseEvent("click"));
document.querySelectorAll(`#${CraftingID.assetGrid} [data-unload-group]`).forEach(e => e.toggleAttribute("data-unload-group", false));
}
// Close the side pannel
document.querySelector(`#${CraftingID.leftPanel} > [aria-checked='true']`)?.dispatchEvent(new Event("click"));
@ -1951,8 +2037,10 @@ function CraftingExit(allowPanelClose=true) {
return;
case "Name": {
const activePanel = document.querySelector(`#${CraftingID.leftPanel} > [aria-checked='true']`);
if (activePanel && allowPanelClose) {
activePanel.dispatchEvent(new Event("click"));
const activeGroup = document.querySelector(`#${CraftingID.centerPanel} [aria-checked='true']`);
if ((activePanel || activeGroup) && allowPanelClose) {
activePanel?.dispatchEvent(new MouseEvent("click"));
activeGroup?.dispatchEvent(new MouseEvent("click"));
} else {
CraftingExitResetElements();
CraftingUnload();
@ -1964,6 +2052,7 @@ function CraftingExit(allowPanelClose=true) {
case "Slot": {
ElementRemove(CraftingID.root);
CharacterDelete(CraftingPreview);
CraftingElements._SearchCache.clear();
CraftingPreview = null;
CraftingOffset = 0;
CraftingDestroy = false;

View file

@ -13,6 +13,7 @@ NoLock,No lock
SelectSlot,"Select an empty slot to craft a new item, or click on the item to edit. Page"
SelectDestroy,Select a crafted item slot to destroy. Page
SelectItem,Select an item
SelectItemSuffix,for the
SelectProperty,Select an item property
SelectLock,Select a lock
SelectName,Configure the crafted item
@ -83,3 +84,4 @@ Upload,Import crafting code
UploadPrompt,Please paste the crafting code. Importing crafting codes will overwrite existing settings. Are you sure?
UploadSucces,Crafting code successfully parsed
UploadFailure,Failed to parse the passed crafting code
Undress,Undress character preview

1 Exit Return to the main hall
13 SelectSlot Select an empty slot to craft a new item, or click on the item to edit. Page
14 SelectDestroy Select a crafted item slot to destroy. Page
15 SelectItem Select an item
16 SelectItemSuffix for the
17 SelectProperty Select an item property
18 SelectLock Select a lock
19 SelectName Configure the crafted item
84 UploadPrompt Please paste the crafting code. Importing crafting codes will overwrite existing settings. Are you sure?
85 UploadSucces Crafting code successfully parsed
86 UploadFailure Failed to parse the passed crafting code
87 Undress Undress character preview