mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-14 04:19:22 +00:00
Merge branch 'craft-focus' into 'master'
ENH: Add a focus grid for filtering crafting items by group See merge request BondageProjects/Bondage-College!5493
This commit is contained in:
commit
8625a09b8e
5 changed files with 250 additions and 42 deletions
BondageClub
|
@ -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));
|
||||
|
|
|
@ -320,6 +320,31 @@
|
|||
padding-inline: 0.15em 0.15em;
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radiogroup"] {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: min(0.3vh, 0.15vw) solid #80808040;
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radio"] {
|
||||
position: absolute;
|
||||
border: min(0.3vh, 0.15vw) solid #80808040;
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radio"][data-blocked] {
|
||||
border-color: #88000580;
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radio"][data-equipped] {
|
||||
border-color: #D5A30080;
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radio"][aria-checked="true"] {
|
||||
border-width: min(0.5vh, 0.25vw);
|
||||
border-color: cyan;
|
||||
}
|
||||
|
||||
@supports(height: 100dvh) {
|
||||
.dialog-root {
|
||||
--menu-button-size: min(9dvh, 4.5dvw);
|
||||
|
@ -334,6 +359,18 @@
|
|||
.dialog-grid-button > .button-tooltip {
|
||||
width: calc(400% + 4 * min(0.2dvh, 0.1dvw) + 3 * var(--gap));
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radiogroup"] {
|
||||
border-width: min(0.3dvh, 0.15dvw);
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radio"] {
|
||||
border-width: min(0.3dvh, 0.15dvw);
|
||||
}
|
||||
|
||||
.dialog-focus-grid [role="radio"][aria-checked="true"] {
|
||||
border-width: min(0.5dvh, 0.25dvw);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (background-color: color-mix(in srgb, black 50%, transparent)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
|
@ -4481,6 +4481,81 @@ function DialogDrawTopMenu(C) {
|
|||
}
|
||||
}
|
||||
|
||||
var DialogFocusGroup = {
|
||||
/**
|
||||
*
|
||||
* @param {string} id - The ID for the to-be created focus group grid
|
||||
* @param {(this: HTMLButtonElement, ev: MouseEvent) => any} listener - The listener to-be executed upon selecting a group; the group name can be retrieved from `this.name`
|
||||
* @param {null | { required?: boolean, useDynamicGroupName?: boolean }} options - Further options for the to-be created focus group grid
|
||||
* @returns {HTMLElement} - The created element
|
||||
*/
|
||||
Create(id, listener, options=null) {
|
||||
options ??= {};
|
||||
const root = document.getElementById(id);
|
||||
if (root) {
|
||||
console.error(`Element "${id}" already exists`);
|
||||
return root;
|
||||
}
|
||||
|
||||
let top = Infinity;
|
||||
let bottom = 0;
|
||||
let left = Infinity;
|
||||
let right = 0;
|
||||
|
||||
/** @type {{ group: AssetGroup, index: number, zone: RectTuple }[]} */
|
||||
const grid = [];
|
||||
for (const group of AssetGroup) {
|
||||
for (const [index, zone] of (group.Zone ?? []).entries()) {
|
||||
grid.push({ group, index, zone});
|
||||
left = Math.min(left, zone[0]);
|
||||
right = Math.max(right, zone[0] + zone[2]);
|
||||
top = Math.min(top, zone[1]);
|
||||
bottom = Math.max(bottom, zone[1] + zone[3]);
|
||||
}
|
||||
}
|
||||
grid.sort((a, b) => (a.zone[1] - b.zone[1]) || (a.zone[0] - b.zone[0]));
|
||||
|
||||
const width = right - left;
|
||||
const height = bottom - top;
|
||||
|
||||
const children = grid.map(({ group, index, zone }, i) => ElementButton.Create(
|
||||
`${id}-${group.Name}-${index}`,
|
||||
listener,
|
||||
{ noStyling: true, role: "radio" },
|
||||
{ button: {
|
||||
attributes: {
|
||||
name: options.useDynamicGroupName ? group.DynamicGroupName : group.Name,
|
||||
tabindex: i === 0 ? 0 : -1,
|
||||
"aria-hidden": index !== 0 ? "true" : undefined,
|
||||
"aria-label": group.Description,
|
||||
},
|
||||
style: {
|
||||
left: `${100 * (zone[0] - left) / width}%`,
|
||||
top: `${100 * (zone[1] - top) / height}%`,
|
||||
width: `${100 * (zone[2] / width)}%`,
|
||||
height: `${100 * (zone[3] / height)}%`,
|
||||
},
|
||||
}},
|
||||
));
|
||||
|
||||
return ElementCreate({
|
||||
tag: "div",
|
||||
attributes: { id },
|
||||
classList: ["dialog-focus-grid"],
|
||||
children: [{
|
||||
tag: "div",
|
||||
children,
|
||||
attributes: {
|
||||
id: `${id}-radiogroup`,
|
||||
role: "radiogroup",
|
||||
"aria-required": options.required ? "true" : "false",
|
||||
"aria-label": "Select focus group",
|
||||
},
|
||||
}],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the left menu for the character
|
||||
* @param {Character} C - The currently focused character
|
||||
|
|
Loading…
Add table
Reference in a new issue