diff --git a/app/assets/javascripts/select-kit/addon/components/category-chooser.js b/app/assets/javascripts/select-kit/addon/components/category-chooser.js index 3dcd570b568..3060665a21d 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/category-chooser.js @@ -6,6 +6,7 @@ import { setting } from "discourse/lib/computed"; import Category from "discourse/models/category"; import PermissionType from "discourse/models/permission-type"; import I18n from "discourse-i18n"; +import CategoryRow from "select-kit/components/category-row"; import ComboBoxComponent from "select-kit/components/combo-box"; export default ComboBoxComponent.extend({ @@ -40,7 +41,7 @@ export default ComboBoxComponent.extend({ }, modifyComponentForRow() { - return "category-row"; + return CategoryRow; }, modifyNoSelection() { diff --git a/app/assets/javascripts/select-kit/addon/components/category-drop.js b/app/assets/javascripts/select-kit/addon/components/category-drop.js index fa456a38b14..5afd206e174 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-drop.js +++ b/app/assets/javascripts/select-kit/addon/components/category-drop.js @@ -8,6 +8,7 @@ import DiscourseURL, { } from "discourse/lib/url"; import Category from "discourse/models/category"; import I18n from "discourse-i18n"; +import CategoryRow from "select-kit/components/category-row"; import ComboBoxComponent from "select-kit/components/combo-box"; export const NO_CATEGORIES_ID = "no-categories"; @@ -41,7 +42,7 @@ export default ComboBoxComponent.extend({ }, modifyComponentForRow() { - return "category-row"; + return CategoryRow; }, displayCategoryDescription: computed(function () { diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.gjs b/app/assets/javascripts/select-kit/addon/components/category-row.gjs new file mode 100644 index 00000000000..e0dea4f0485 --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/category-row.gjs @@ -0,0 +1,309 @@ +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 { inject as 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"; + +export default class CategoryRow extends Component { + @service site; + @service siteSettings; + + 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 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; + } + + get displayCategoryDescription() { + const option = this.args.selectKit.options.displayCategoryDescription; + if (isNone(option)) { + return true; + } + + return option; + } + + get title() { + if (this.category) { + return this.categoryName; + } + } + + get categoryName() { + return this.category.name; + } + + get categoryDescriptionText() { + return this.category.description_text; + } + + @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: this.categoryLink, + allowUncategorized: + this.allowUncategorizedTopics || this.allowUncategorized, + hideParent: !!this.parentCategory, + topicCount: this.topicCount, + }) + ); + } + + @cached + get badgeForParentCategory() { + return htmlSafe( + categoryBadgeHTML(this.parentCategory, { + link: this.categoryLink, + allowUncategorized: + this.allowUncategorizedTopics || this.allowUncategorized, + recursive: true, + }) + ); + } + + get parentCategory() { + return Category.findById(this.parentCategoryId); + } + + 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(); + 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(); + + this.args.selectKit.select( + this.args.selectKit.highlighted.id, + this.args.selectKit.highlighted + ); + event.preventDefault(); + } 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); + } + + +} diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.hbs b/app/assets/javascripts/select-kit/addon/components/category-row.hbs deleted file mode 100644 index 35670d94ae2..00000000000 --- a/app/assets/javascripts/select-kit/addon/components/category-row.hbs +++ /dev/null @@ -1,19 +0,0 @@ -{{#if this.category}} - - - {{#if this.shouldDisplayDescription}} - - {{/if}} -{{else}} - {{html-safe this.label}} -{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.js b/app/assets/javascripts/select-kit/addon/components/category-row.js deleted file mode 100644 index 2aded4a5ed8..00000000000 --- a/app/assets/javascripts/select-kit/addon/components/category-row.js +++ /dev/null @@ -1,120 +0,0 @@ -import { computed } from "@ember/object"; -import { bool, reads } from "@ember/object/computed"; -import { htmlSafe } from "@ember/template"; -import { isEmpty, isNone } from "@ember/utils"; -import { categoryBadgeHTML } from "discourse/helpers/category-link"; -import { setting } from "discourse/lib/computed"; -import Category from "discourse/models/category"; -import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; - -export default SelectKitRowComponent.extend({ - classNames: ["category-row"], - hideParentCategory: bool("selectKit.options.hideParentCategory"), - allowUncategorized: bool("selectKit.options.allowUncategorized"), - categoryLink: bool("selectKit.options.categoryLink"), - countSubcategories: bool("selectKit.options.countSubcategories"), - allowUncategorizedTopics: setting("allow_uncategorized_topics"), - - displayCategoryDescription: computed( - "selectKit.options.displayCategoryDescription", - function () { - const option = this.selectKit.options.displayCategoryDescription; - if (isNone(option)) { - return true; - } - - return option; - } - ), - - title: computed("categoryName", function () { - if (this.category) { - return this.categoryName; - } - }), - categoryName: reads("category.name"), - - categoryDescriptionText: reads("category.description_text"), - - category: computed("rowValue", "rowName", function () { - if (isEmpty(this.rowValue)) { - const uncategorized = Category.findUncategorized(); - if (uncategorized && uncategorized.name === this.rowName) { - return uncategorized; - } - } else { - return Category.findById(parseInt(this.rowValue, 10)); - } - }), - - badgeForCategory: computed("category", "parentCategory", function () { - return htmlSafe( - categoryBadgeHTML(this.category, { - link: this.categoryLink, - allowUncategorized: - this.allowUncategorizedTopics || this.allowUncategorized, - hideParent: !!this.parentCategory, - topicCount: this.topicCount, - }) - ); - }), - - badgeForParentCategory: computed("parentCategory", function () { - return htmlSafe( - categoryBadgeHTML(this.parentCategory, { - link: this.categoryLink, - allowUncategorized: - this.allowUncategorizedTopics || this.allowUncategorized, - recursive: true, - }) - ); - }), - - parentCategory: computed("parentCategoryId", function () { - return Category.findById(this.parentCategoryId); - }), - - hasParentCategory: bool("parentCategoryId"), - - parentCategoryId: reads("category.parent_category_id"), - - categoryTotalTopicCount: reads("category.totalTopicCount"), - - categoryTopicCount: reads("category.topic_count"), - - topicCount: computed( - "categoryTotalTopicCount", - "categoryTopicCount", - "countSubcategories", - function () { - return this.countSubcategories - ? this.categoryTotalTopicCount - : this.categoryTopicCount; - } - ), - - shouldDisplayDescription: computed( - "displayCategoryDescription", - "categoryDescriptionText", - function () { - return ( - this.displayCategoryDescription && - this.categoryDescriptionText && - this.categoryDescriptionText !== "null" - ); - } - ), - - descriptionText: computed("categoryDescriptionText", function () { - if (this.categoryDescriptionText) { - return this._formatDescription(this.categoryDescriptionText); - } - }), - - _formatDescription(description) { - const limit = 200; - return `${description.slice(0, limit)}${ - description.length > limit ? "…" : "" - }`; - }, -}); diff --git a/app/assets/javascripts/select-kit/addon/components/category-selector.js b/app/assets/javascripts/select-kit/addon/components/category-selector.js index 4d54036e425..b6cb5a73b09 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-selector.js +++ b/app/assets/javascripts/select-kit/addon/components/category-selector.js @@ -2,6 +2,7 @@ import { computed } from "@ember/object"; import { mapBy } from "@ember/object/computed"; import Category from "discourse/models/category"; import { makeArray } from "discourse-common/lib/helpers"; +import CategoryRow from "select-kit/components/category-row"; import MultiSelectComponent from "select-kit/components/multi-select"; export default MultiSelectComponent.extend({ @@ -46,7 +47,7 @@ export default MultiSelectComponent.extend({ value: mapBy("categories", "id"), modifyComponentForRow() { - return "category-row"; + return CategoryRow; }, async search(filter) { diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit.js b/app/assets/javascripts/select-kit/addon/components/select-kit.js index c973b17fc48..9266e598abc 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -768,6 +768,8 @@ export default Component.extend( } else { if (this.selectKit.isFilterExpanded) { this._focusFilter(); + this.set("selectKit.highlighted", null); + return; } else { highlightedIndex = 0; } @@ -791,6 +793,8 @@ export default Component.extend( } else { if (this.selectKit.isFilterExpanded) { this._focusFilter(); + this.set("selectKit.highlighted", null); + return; } else { highlightedIndex = count - 1; } diff --git a/app/assets/javascripts/select-kit/package.json b/app/assets/javascripts/select-kit/package.json index b74154571f0..858ba84db20 100644 --- a/app/assets/javascripts/select-kit/package.json +++ b/app/assets/javascripts/select-kit/package.json @@ -16,7 +16,8 @@ "dependencies": { "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", - "ember-cli-htmlbars": "^6.3.0" + "ember-cli-htmlbars": "^6.3.0", + "ember-template-imports": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.23.7",