Category dropdown wip

This commit is contained in:
Brandon Thomas committed 2023-01-18 20:08:05 -05:00
1 parent 9ff6005d48
commit d2df8d6e8b
1 file changed
+300 -47
@@ -34,6 +34,143 @@ interface Props {
}
export function CategoryOptions(props: Props) {
const {
allTtsCategories,
allTtsModels,
allTtsCategoriesByTokenMap,
allTtsModelsByTokenMap,
ttsModelsByCategoryToken,
dropdownCategories,
setDropdownCategories,
selectedCategories,
setSelectedCategories,
maybeSelectedTtsModel,
} = props;
const { t } = useTranslation();
const isLoading = allTtsModels.length === 0;
useEffect(() => {
// NB: Dropdowns do not seem to respect React very well.
// Despite setting <select>'s value and defaultValue, and <option>'s selected=true,
// the dropdowns are left in a default state. I'll use the post-render side effect
// to select the correct options.
let selectedModelToken = undefined;
if (maybeSelectedTtsModel) {
selectedModelToken = maybeSelectedTtsModel.model_token;
} else if (allTtsModels.length > 0) {
// TODO: Move the initial model selection logic here, perhaps.
//selectedModelToken = allTtsModels[0].model_token;
}
let maybeElement = document.getElementsByName("tts-model-select")[0];
if (!!maybeElement && !!selectedModelToken) {
(maybeElement as any).value = selectedModelToken;
} else if (isLoading) {
(maybeElement as any).value = ""; // Empty string will match "loading" <option>
}
let categoryDropdownElements =
document.getElementsByClassName("category-dropdown");
const iterLength = Math.min(
selectedCategories.length,
categoryDropdownElements.length
);
for (let i = 0; i < iterLength; i++) {
let categoryDropdownElement = categoryDropdownElements[i];
let selectedCategory = selectedCategories[i];
if (!categoryDropdownElements || !selectedCategory) {
break;
}
(categoryDropdownElement as any).value = selectedCategory.category_token;
}
});
const doChangeCategory = (level: number, maybeToken: string) => {
// Slice off all the irrelevant child category choices, then append new choice.
let newCategorySelections = selectedCategories.slice(0, level);
// And the dropdowns themselves
let newDropdownCategories = dropdownCategories.slice(0, level + 1);
let category = allTtsCategoriesByTokenMap.get(maybeToken);
if (!!category) {
newCategorySelections.push(category);
}
setSelectedCategories(newCategorySelections);
const newSubcategories = allTtsCategories.filter((category) => {
return category.maybe_super_category_token === maybeToken;
});
newDropdownCategories.push(newSubcategories);
setDropdownCategories(newDropdownCategories);
// We might have switched into a category without our selected TTS model.
// If so, pick a new TTS model.
let maybeNewModel = undefined;
const availableModelsForCategory = ttsModelsByCategoryToken.get(maybeToken);
if (!!availableModelsForCategory && !!maybeSelectedTtsModel) {
const modelValid = availableModelsForCategory.has(maybeSelectedTtsModel);
if (!modelValid) {
maybeNewModel = Array.from(availableModelsForCategory)[0];
}
}
if (!!maybeNewModel) {
props.setMaybeSelectedTtsModel(maybeNewModel);
}
};
const handleChangeCategory = (
level: number,
maybeCategoryToken?: string,
) => {
if (!maybeCategoryToken) {
return true;
}
doChangeCategory(level, maybeCategoryToken);
return true;
};
const handleRemoveCategory = (level: number) => {
let parentLevel = Math.max(level - 1, 0);
let maybeToken = "*"; // NB: Sentinel for the "All Voices" / "Select..." <option>
doChangeCategory(parentLevel, maybeToken);
// NB: There's a bug selecting the "default" of the parent category.
// React won't respect the state, so we'll brute force it here.
let maybeElement = document.getElementsByName(
`categories-${parentLevel}`
)[0];
if (maybeElement) {
(maybeElement as any).value = "*";
}
};
const handleChangeVoice = (ev: React.FormEvent<HTMLSelectElement>) => {
const ttsModelToken = (ev.target as HTMLSelectElement).value;
const maybeTtsModel = allTtsModelsByTokenMap.get(ttsModelToken);
if (maybeTtsModel) {
props.setMaybeSelectedTtsModel(maybeTtsModel);
}
};
let categoryDropdowns = buildDropdowns(
dropdownCategories,
ttsModelsByCategoryToken,
handleChangeCategory,
handleRemoveCategory);
const options = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
@@ -44,52 +181,168 @@ export function CategoryOptions(props: Props) {
];
return (
<div className="d-flex flex-column flex-md-row gap-2">
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
className="w-100"
/>
</div>
<div className="d-none d-md-flex align-items-center">
<FontAwesomeIcon
icon={faArrowRightLong}
className="fs-6 opacity-75"
/>
</div>
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
className="w-100"
/>
</div>
<div className="d-none d-md-flex align-items-center">
<FontAwesomeIcon
icon={faArrowRightLong}
className="fs-6 opacity-75"
/>
</div>
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
className="w-100"
/>
</div>
</div>
<>
<div className="d-flex flex-column flex-md-row gap-2">
{categoryDropdowns}
</div>
<br />
<hr />
<br />
<div className="d-flex flex-column flex-md-row gap-2">
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
className="w-100"
/>
</div>
<div className="d-none d-md-flex align-items-center">
<FontAwesomeIcon
icon={faArrowRightLong}
className="fs-6 opacity-75"
/>
</div>
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
className="w-100"
/>
</div>
<div className="d-none d-md-flex align-items-center">
<FontAwesomeIcon
icon={faArrowRightLong}
className="fs-6 opacity-75"
/>
</div>
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
className="w-100"
/>
</div>
</div>
</>
);
}
function buildDropdowns(
dropdownCategories: TtsCategoryType[][],
ttsModelsByCategoryToken: Map<string, Set<TtsModelListItem>>,
handleChangeCategory : (i: number, categoryToken?: string) => void,
handleRemoveCategory : (i: number) => void,
) {
let categoryDropdowns = [];
for (let i = 0; i < dropdownCategories.length; i++) {
const currentDropdownCategories = dropdownCategories[i];
let defaultName = i === 0 ? "All Voices" : "Select...";
let dropdownOptions = [];
dropdownOptions.push(
<option key={`option-${i}-*`} value="*">
{defaultName}
</option>
);
const options = currentDropdownCategories.map((category) => {
return {
value: category.category_token,
label: category.name_for_dropdown,
}
});
currentDropdownCategories.forEach((category) => {
const models = ttsModelsByCategoryToken.get(category.category_token);
if (models === undefined || models.size === 0) {
return; // If there are no models at the leaves, skip
}
dropdownOptions.push(
<option
key={`option-${i}-${category.category_token}`}
value={category.category_token}
>
{category.name_for_dropdown}
</option>
);
});
if (dropdownOptions.length <= 1) {
// We've run out of subcategories. (1 == "Select...")
// No sense trying to build more.
break;
}
const selectCssClasses = "category-dropdown form-select"; // NB: 'category-dropdown' is important for function.
const xButtonCssClasses = "btn btn-destructive btn-inform";
// categoryDropdowns.push(
// <React.Fragment key={`categoryDropdown-${i}`}>
// <div className="d-flex gap-3 align-items-center mb-4 w-100">
// <div className="form-group input-icon w-100">
// <select
// onClick={() => {
// Analytics.ttsClickSelectCategory();
// }}
// className={selectCssClasses}
// name={`categories-${i}`}
// onChange={(ev) => handleChangeCategory(ev, i)}
// defaultValue="*"
// >
// {dropdownOptions}
// </select>
// <span className="form-control-feedback">
// <FontAwesomeIcon icon={faTags} />
// </span>
// </div>
//
// <div className="control">
// <FontAwesomeIcon icon={faCaretRight} size="2x" color="#999" />
// </div>
// </div>
// </React.Fragment>
// );
categoryDropdowns.push(
<React.Fragment key={`categoryDropdown-${i}`}>
<div className="w-100">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faTags} />
</span>
<Select
defaultValue={options[2]}
options={options}
classNames={SearchFieldClass}
onChange={(option) => handleChangeCategory(i, option?.value)}
className="w-100"
/>
</div>
</React.Fragment>
);
}
return categoryDropdowns;
}