diff --git a/BondageClub/Scripts/Dialog.js b/BondageClub/Scripts/Dialog.js index 8331b15fd53c12790bc8bbb97f6e6a8a20c3f538..d8d7314b62131dcce0934c40b345bf46e6f5886f 100644 --- a/BondageClub/Scripts/Dialog.js +++ b/BondageClub/Scripts/Dialog.js @@ -2930,7 +2930,7 @@ class DialogMenu { * @type {(this: HTMLButtonElement, ev: MouseEvent) => void} */ _ClickPaginatePrev: function(event) { - document.getElementById("MainCanvas")?.dispatchEvent(new KeyboardEvent("keydown", { key: "PageUp" })) + document.getElementById("MainCanvas")?.dispatchEvent(new KeyboardEvent("keydown", { key: "PageUp" })); }, /** @@ -2938,7 +2938,7 @@ class DialogMenu { * @type {(this: HTMLButtonElement, ev: MouseEvent) => void} */ _ClickPaginateNext: function(event) { - document.getElementById("MainCanvas")?.dispatchEvent(new KeyboardEvent("keydown", { key: "PageDown" })) + document.getElementById("MainCanvas")?.dispatchEvent(new KeyboardEvent("keydown", { key: "PageDown" })); }, }; } @@ -3405,14 +3405,34 @@ class _DialogItemMenu extends DialogMenu { for (const [i, button] of Array.from(buttonGrid.children).entries()) { const clickedItem = DialogInventory[i]; + // Tasks already performed during a full `options.resetDialogItems` operation + if (!options.resetDialogItems) { + const wasWorn = clickedItem.Worn; + const isWorn = !!(equippedItem && !DialogAllowItemClick(equippedItem, clickedItem)); + if (isWorn) { + Object.assign(clickedItem, equippedItem); + } + if (isWorn !== wasWorn) { + clickedItem.Worn = isWorn; + button.setAttribute("aria-checked", isWorn.toString()); + } + } + const status = this.GetClickStatus(this.C, clickedItem, equippedItem); if (status) { button.setAttribute("aria-disabled", "true"); } else { button.removeAttribute("aria-disabled"); } + + if (options.reset) { + continue; + } + + // Tasks already performed during a full `options.reset` operation button.toggleAttribute("data-hidden", CharacterAppearanceItemIsHidden(clickedItem.Asset.Name, clickedItem.Asset.Group.Name)); button.toggleAttribute("data-vibrating", clickedItem.Property?.Effect?.includes("Vibrating") ?? false); + ElementButton.ReloadAssetIcons(/** @type {HTMLButtonElement} */(button), clickedItem, this.C); } if (options.resetScrollbar) { @@ -3542,7 +3562,12 @@ class _DialogLockingMenu extends DialogMenu { } else { button.removeAttribute("aria-disabled"); } - button.toggleAttribute("data-hidden", CharacterAppearanceItemIsHidden(clickedLock.Asset.Name, clickedLock.Asset.Group.Name)); + + if (!options.reset) { + // Tasks already performed during a full `options.reset` + button.toggleAttribute("data-hidden", CharacterAppearanceItemIsHidden(clickedLock.Asset.Name, clickedLock.Asset.Group.Name)); + ElementButton.ReloadAssetIcons(/** @type {HTMLButtonElement} */(button), clickedLock, this.C); + } } if (options.resetScrollbar) { @@ -3632,12 +3657,25 @@ class _DialogPermissionMenu extends DialogMenu { )); } + const equippedItem = InventoryGet(this.C, this.focusGroup.Name); for (const [i, button] of Array.from(buttonGrid.children).entries()) { const clickedItem = DialogInventory[i]; + const status = this.GetClickStatus(this.C, clickedItem, equippedItem); + if (status) { + button.setAttribute("aria-disabled", "true"); + } else { + button.removeAttribute("aria-disabled"); + } + const permissions = this.C.PermissionItems[`${clickedItem.Asset.Group.Name}/${clickedItem.Asset.Name}`]; button.setAttribute("data-permission", permissions?.Permission ?? "Default"); - button.toggleAttribute("data-hidden", CharacterAppearanceItemIsHidden(clickedItem.Asset.Name, clickedItem.Asset.Group.Name)); + + if (!options.reset) { + // Tasks already performed during a full `options.reset` + button.toggleAttribute("data-hidden", CharacterAppearanceItemIsHidden(clickedItem.Asset.Name, clickedItem.Asset.Group.Name)); + ElementButton.ReloadAssetIcons(/** @type {HTMLButtonElement} */(button), clickedItem, this.C); + } } if (options.resetScrollbar) { @@ -3673,7 +3711,11 @@ class _DialogActivitiesMenu extends DialogMenu { }); /** @type {DialogMenu<T, ItemActivity>["clickStatusCallbacks"]} */ - clickStatusCallbacks = {}; + clickStatusCallbacks = { + IsBlocked: (C, clickedActivity, equippedItem) => { + return clickedActivity.Blocked === "blocked" ? InterfaceTextGet("ExtendedItemNoItemPermission") : null; + }, + }; _Load() { const ids = this.ids; @@ -3724,9 +3766,12 @@ class _DialogActivitiesMenu extends DialogMenu { )); } + const equippedItem = InventoryGet(this.C, this.focusGroup.Name); for (const [i, button] of Array.from(buttonGrid.children).entries()) { const clickedActivity = DialogActivity[i]; - if (clickedActivity.Blocked === "blocked") { + + const status = this.GetClickStatus(this.C, clickedActivity, equippedItem); + if (status) { button.setAttribute("aria-disabled", "true"); } else { button.removeAttribute("aria-disabled"); diff --git a/BondageClub/Scripts/Element.js b/BondageClub/Scripts/Element.js index f20458347c4dfc163a61f7279e28416b0018ecf4..0918e37c38b94e5fadc9985d3c1df5adf30276a9 100644 --- a/BondageClub/Scripts/Element.js +++ b/BondageClub/Scripts/Element.js @@ -1145,7 +1145,7 @@ var ElementButton = { /** * Parse the passed icon list, returning its corresponding `<img>` grid and tooltip if non-empty * @param {string} id - The ID of the parent element - * @param {readonly (InventoryIcon | { iconSrc: string, tooltipText: string | Node })[]} [icons] - The (optional) list of icons + * @param {readonly (InventoryIcon | { name: string, iconSrc: string, tooltipText: string | Node })[]} [icons] - The (optional) list of icons * @returns {null | { iconGrid: HTMLDivElement, tooltip: (string | HTMLElement)[] }} - `null` if the provided icon list is empty and otherwise an object containing the icon grid and a icon-specific tooltip */ _ParseIcons: function _ParseIcons(id, icons) { @@ -1154,50 +1154,61 @@ var ElementButton = { return null; } - const tooltip = ElementCreate({ + const tooltip = document.getElementById(`${id}-icon-ul`) ?? ElementCreate({ tag: "ul", attributes: { id: `${id}-icon-ul` }, classList: ["button-icon-tooltip-ul"], children: [], }); - const iconGrid = ElementCreate({ + + const iconGrid = /** @type {HTMLDivElement} */(document.getElementById(`${id}-icon-grid`)) ?? ElementCreate({ tag: "div", classList: ["button-icon-grid"], - attributes: { "aria-hidden": "true" }, - children: icons.map((icon) => { - /** @type {string} */ - let src; - /** @type {string | Node} */ - let tooltipText; - if (typeof icon === "object") { - src = icon.iconSrc; - tooltipText = icon.tooltipText; - } else if (icon.endsWith("Padlock")) { - src = `./Assets/Female3DCG/ItemMisc/Preview/${icon}.png`; - tooltipText = InterfaceTextGet("PreviewIconPadlock").replace( - "AssetName", - AssetGet("Female3DCG", "ItemMisc", icon).Description, - ); - } else { - src = `./Icons/Previews/${icon}.png`; - tooltipText = InterfaceTextGet(`PreviewIcon${icon}`); - } + attributes: { id: `${id}-icon-grid`, "aria-hidden": "true" }, + }); - ElementCreate({ - tag: "li", - attributes: { id: `${id}-icon-li-${icon}` }, - classList: ["button-icon-tooltip-li"], - children: [tooltipText], - parent: tooltip, - style: { "background-image": `url("./${src}")` }, - }); - - return { - tag: "img", - classList: ["button-icon"], - attributes: { decoding: "async", loading: "lazy", src }, - }; - }), + icons.forEach((icon) => { + let custom = false; + /** @type {string} */ + let name; + /** @type {string} */ + let src; + /** @type {string | Node} */ + let tooltipText; + if (typeof icon === "object") { + custom = true; + name = icon.name; + src = icon.iconSrc; + tooltipText = icon.tooltipText; + } else if (icon.endsWith("Padlock")) { + name = icon; + src = `./Assets/Female3DCG/ItemMisc/Preview/${icon}.png`; + tooltipText = InterfaceTextGet("PreviewIconPadlock").replace( + "AssetName", + AssetGet("Female3DCG", "ItemMisc", icon).Description, + ); + } else { + name = icon; + src = `./Icons/Previews/${icon}.png`; + tooltipText = InterfaceTextGet(`PreviewIcon${icon}`); + } + + ElementCreate({ + tag: "li", + attributes: { id: `${id}-icon-li-${name}` }, + classList: ["button-icon-tooltip-li"], + children: [tooltipText], + parent: tooltip, + style: { "background-image": `url("./${src}")` }, + }); + + ElementCreate({ + tag: "img", + classList: ["button-icon"], + attributes: { decoding: "async", loading: "lazy", src, "aria-owns": `${id}-icon-li-${name}` }, + dataAttributes: { name, custom: custom ? "" : undefined }, + parent: iconGrid, + }); }); return { iconGrid, tooltip: [InterfaceTextGet("StatusAndEffects"), ElementCreate({ tag: "br" }), tooltip] }; }, @@ -1466,6 +1477,45 @@ var ElementButton = { options.tooltip ??= options.icons.length ? [""] : undefined; return ElementButton.Create(id, onClick, options, htmlOptions); }, + + /** + * Reload the icons of the passed {@link ElementButton.CreateForAsset} button based on the items & characters current state. + * @param {HTMLButtonElement} button - The button in question + * @param {Asset | Item} asset - The asset (or item) for linked to the button + * @param {null | Character} C - The character wearing the asset/item (if any) + * @returns {boolean} - Whether the icons were updated or not + */ + ReloadAssetIcons: function ReloadAssetIcons(button, asset, C) { + const item = "Asset" in asset ? asset : { Asset: asset }; + asset = item.Asset; + + const icons = Array.from(button.querySelectorAll(".button-icon")); + const iconNamesOld = icons.map(el => el.getAttribute("data-name")); + const iconNamesNew = [ + DialogGetFavoriteStateDetails(C ?? Player, asset)?.Icon, + ...DialogGetLockIcon(item, "Property" in item), + ...DialogGetAssetIcons(asset), + ...DialogEffectIcons.GetIcons(item), + ]; + + const iconNamesAdded = iconNamesNew.filter(i => i != null && !iconNamesOld.includes(i)); + const iconNamesRemoved = iconNamesOld.filter(i => !/** @type {string[]} */(iconNamesNew).includes(i)); + if (iconNamesAdded.length === 0 && iconNamesRemoved.length === 0) { + return false; + } + console.log(true, button, asset, C, iconNamesAdded, iconNamesRemoved); + + for (const icon of icons) { + if (!icon.hasAttribute("data-custom")) { + const tooltipComponentID = icon.getAttribute("aria-owns"); + if (tooltipComponentID) { document.getElementById(tooltipComponentID)?.remove(); } + icon.remove(); + } + } + + ElementButton._ParseIcons(button.id, iconNamesAdded); + return true; + }, }; /** diff --git a/BondageClub/Scripts/Typedef.d.ts b/BondageClub/Scripts/Typedef.d.ts index 828f4cecd73787ecdf71b1f65da76092fdd66402..47f31efe75220245c0e161c422e4b83427a5b170 100644 --- a/BondageClub/Scripts/Typedef.d.ts +++ b/BondageClub/Scripts/Typedef.d.ts @@ -139,7 +139,7 @@ declare namespace ElementButton { * * Alternatively, one can directly pass the icon's {@link HTMLImageElement.src} and its tooltip component. */ - icons?: readonly (null | InventoryIcon | { iconSrc: string, tooltipText: string | Node })[]; + icons?: readonly (null | InventoryIcon | { iconSrc: string, tooltipText: string | Node, name: string })[]; /** The role of the button. All accepted values are currently special-cased in order to set role-specific event listeners and/or attributes. */ role?: "radio" | "checkbox" | "menuitemradio" | "menuitemcheckbox"; /** Whether to limit the default styling of the button's border and background */