diff --git a/BondageClub/CSS/Crafting.css b/BondageClub/CSS/Crafting.css index 34be9db9d7..8767add8c5 100644 --- a/BondageClub/CSS/Crafting.css +++ b/BondageClub/CSS/Crafting.css @@ -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)); diff --git a/BondageClub/Screens/Room/Crafting/Crafting.js b/BondageClub/Screens/Room/Crafting/Crafting.js index f784dbdf7b..5216b59b75 100644 --- a/BondageClub/Screens/Room/Crafting/Crafting.js +++ b/BondageClub/Screens/Room/Crafting/Crafting.js @@ -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; diff --git a/BondageClub/Screens/Room/Crafting/Text_Crafting.csv b/BondageClub/Screens/Room/Crafting/Text_Crafting.csv index bb7d743a0a..dced7db9df 100644 --- a/BondageClub/Screens/Room/Crafting/Text_Crafting.csv +++ b/BondageClub/Screens/Room/Crafting/Text_Crafting.csv @@ -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