discourse/js/select-kit/addon/components/category-row.gjs
2025-10-20 14:44:46 +01:00

318 lines
7.9 KiB
Text
Vendored

import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { guidFor } from "@ember/object/internals";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { isEmpty, isNone } from "@ember/utils";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
import concatClass from "discourse/helpers/concat-class";
import dirSpan from "discourse/helpers/dir-span";
import Category from "discourse/models/category";
import { i18n } from "discourse-i18n";
export default class CategoryRow extends Component {
@service site;
@service siteSettings;
readOnlyDesc = i18n("category_row.read_only_description");
get isNone() {
return this.rowValue === this.args.selectKit?.noneItem;
}
get highlightedValue() {
return this.args.selectKit.get("highlighted.id");
}
get isHighlighted() {
return this.rowValue === this.highlightedValue;
}
get isSelected() {
return this.rowValue === this.args.value;
}
get isReadOnly() {
if (!this.readOnlyCategoryId) {
return false;
}
return this.rowValue === this.readOnlyCategoryId;
}
get readOnlyCategoryId() {
return this.args.selectKit.options.readOnlyCategoryId;
}
get hideParentCategory() {
return this.args.selectKit.options.hideParentCategory;
}
get categoryLink() {
return this.args.selectKit.options.categoryLink;
}
get countSubcategories() {
return this.args.selectKit.options.countSubcategories;
}
get allowUncategorizedTopics() {
return this.siteSettings.hideParentCategory;
}
get allowUncategorized() {
return this.args.selectKit.options.allowUncategorized;
}
get rowName() {
return this.args.item?.name;
}
get rowValue() {
return this.args.item?.id;
}
get guid() {
return guidFor(this.args.item);
}
get label() {
return this.args.item?.name || this.args.item?.label;
}
get displayCategoryDescription() {
const option = this.args.selectKit.options.displayCategoryDescription;
if (isNone(option)) {
return true;
}
return option;
}
get title() {
if (this.category) {
return this.isReadOnly ? this.readOnlyDesc : this.categoryName;
}
}
get categoryName() {
return this.category.displayName;
}
get categoryDescriptionText() {
return this.category.descriptionText;
}
@cached
get category() {
if (isEmpty(this.rowValue)) {
const uncategorized = Category.findUncategorized();
if (uncategorized && uncategorized.name === this.rowName) {
return uncategorized;
}
} else {
return Category.findById(parseInt(this.rowValue, 10));
}
}
@cached
get badgeForCategory() {
return htmlSafe(
categoryBadgeHTML(this.category, {
link: false,
allowUncategorized:
this.allowUncategorizedTopics || this.allowUncategorized,
hideParent: true,
ancestors: this.category?.predecessors,
topicCount: this.topicCount,
subcategoryCount: this.args.item?.category
? this.category.subcategory_count
: 0,
readOnly: this.isReadOnly,
})
);
}
get hasParentCategory() {
return this.parentCategoryId;
}
get parentCategoryId() {
return this.category?.parent_category_id;
}
get categoryTotalTopicCount() {
return this.category?.totalTopicCount;
}
get categoryTopicCount() {
return this.category?.topic_count;
}
get topicCount() {
return this.countSubcategories
? this.categoryTotalTopicCount
: this.categoryTopicCount;
}
get shouldDisplayDescription() {
return (
this.displayCategoryDescription &&
this.categoryDescriptionText &&
this.categoryDescriptionText !== "null"
);
}
@cached
get descriptionText() {
if (this.categoryDescriptionText) {
return this._formatDescription(this.categoryDescriptionText);
}
}
@action
handleMouseEnter() {
if (this.site.mobileView) {
return;
}
if (!this.isDestroying || !this.isDestroyed) {
this.args.selectKit.onHover(this.rowValue, this.args.item);
}
return false;
}
@action
handleClick(event) {
event.preventDefault();
event.stopPropagation();
if (this.isReadOnly) {
return false;
}
this.args.selectKit.select(this.rowValue, this.args.item);
return false;
}
@action
handleMouseDown(event) {
if (this.args.selectKit.options.preventHeaderFocus) {
event.preventDefault();
}
}
@action
handleFocusIn(event) {
event.stopImmediatePropagation();
}
@action
handleKeyDown(event) {
if (this.args.selectKit.isExpanded) {
if (event.key === "Backspace") {
if (this.args.selectKit.isFilterExpanded) {
this.args.selectKit.set(
"filter",
this.args.selectKit.filter.slice(0, -1)
);
this.args.selectKit.triggerSearch();
this.args.selectKit.focusFilter();
event.preventDefault();
event.stopPropagation();
}
} else if (event.key === "ArrowUp") {
this.args.selectKit.highlightPrevious();
event.preventDefault();
} else if (event.key === "ArrowDown") {
this.args.selectKit.highlightNext();
event.preventDefault();
} else if (event.key === "Enter") {
event.stopImmediatePropagation();
event.preventDefault();
if (this.isReadOnly) {
return false;
}
this.args.selectKit.select(
this.args.selectKit.highlighted.id,
this.args.selectKit.highlighted
);
} else if (event.key === "Escape") {
this.args.selectKit.close(event);
this.args.selectKit.headerElement().focus();
event.preventDefault();
event.stopPropagation();
} else {
if (this._isValidInput(event.key)) {
this.args.selectKit.set("filter", event.key);
this.args.selectKit.triggerSearch();
this.args.selectKit.focusFilter();
event.preventDefault();
event.stopPropagation();
}
}
}
}
_formatDescription(description) {
const limit = 200;
return `${description.slice(0, limit)}${
description.length > limit ? "…" : ""
}`;
}
_isValidInput(eventKey) {
// relying on passing the event to the input is risky as it could not work
// dispatching the event won't work as the event won't be trusted
// safest solution is to filter event and prefill filter with it
const nonInputKeysRegex =
/F\d+|Arrow.+|Meta|Alt|Control|Shift|Delete|Enter|Escape|Tab|Space|Insert|Backspace/;
return !nonInputKeysRegex.test(eventKey);
}
<template>
{{! template-lint-disable no-pointer-down-event-binding }}
<div
class={{concatClass
"category-row"
"select-kit-row"
(if this.isSelected "is-selected")
(if this.isHighlighted "is-highlighted")
(if this.isNone "is-none")
}}
role="menuitemradio"
data-index={{@index}}
data-name={{this.rowName}}
data-value={{this.rowValue}}
data-title={{this.title}}
title={{this.title}}
data-guid={{this.guid}}
{{on "focusin" this.handleFocusIn}}
{{on "mousedown" this.handleMouseDown}}
{{on "mouseenter" this.handleMouseEnter passive=true}}
{{on "click" this.handleClick}}
{{on "keydown" this.handleKeyDown}}
aria-checked={{this.isSelected}}
tabindex="-1"
>
{{#if this.category}}
<div class="category-status">
{{this.badgeForCategory}}
</div>
{{#if this.shouldDisplayDescription}}
<div class="category-desc" aria-hidden="true">
{{dirSpan this.descriptionText htmlSafe="true"}}
</div>
{{/if}}
{{else}}
{{htmlSafe this.label}}
{{/if}}
</div>
</template>
}