From 3e3c051164c4c32962d0a725b201784bb9a1f6a1 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 22 Aug 2024 09:39:03 +0100 Subject: [PATCH] DEV: Convert select-kit base classes to native class syntax (#28467) This lays the groundwork for converting SelectKit subclasses to native class syntax. This commit is designed to be entirely backwards-compatible, so it should not affect any existing subclasses. Of interest: - Any properties which are designed to be overridden by subclasses are implemented using a local `@protoProp` decorator. That means they are applied to the prototype, so that they can be overridden in subclasses by both legacy `.extend()` prototype extensions, and by modern native-class fields. - New class decorators are introduced: `@selectKitOptions` and `@pluginApiIdentifiers`. These are native class versions of the legacy `concatenatedProperties` system. This follows the pattern Ember has introduced for `@className`, `@classNameBindings`, etc. --- .../addon/components/multi-select.js | 120 +- .../select-kit/addon/components/select-kit.js | 2333 +++++++++-------- .../addon/components/single-select.js | 71 +- .../select-kit/addon/mixins/plugin-api.js | 7 - 4 files changed, 1276 insertions(+), 1255 deletions(-) diff --git a/app/assets/javascripts/select-kit/addon/components/multi-select.js b/app/assets/javascripts/select-kit/addon/components/multi-select.js index e5606dbadfa..f573dddfaba 100644 --- a/app/assets/javascripts/select-kit/addon/components/multi-select.js +++ b/app/assets/javascripts/select-kit/addon/components/multi-select.js @@ -1,41 +1,45 @@ import { computed } from "@ember/object"; import { next } from "@ember/runloop"; import { isPresent } from "@ember/utils"; +import { classNames } from "@ember-decorators/component"; import { makeArray } from "discourse-common/lib/helpers"; -import SelectKitComponent from "select-kit/components/select-kit"; +import SelectKitComponent, { + pluginApiIdentifiers, + selectKitOptions, +} from "select-kit/components/select-kit"; -export default SelectKitComponent.extend({ - pluginApiIdentifiers: ["multi-select"], - classNames: ["multi-select"], - multiSelect: true, +@classNames("multi-select") +@selectKitOptions({ + none: "select_kit.default_header_text", + clearable: true, + filterable: true, + filterIcon: null, + closeOnChange: false, + autoInsertNoneItem: false, + headerComponent: "multi-select/multi-select-header", + filterComponent: "multi-select/multi-select-filter", + autoFilterable: true, + caretDownIcon: "caretIcon", + caretUpIcon: "caretIcon", + useHeaderFilter: false, +}) +@pluginApiIdentifiers(["multi-select"]) +export default class MultiSelect extends SelectKitComponent { + multiSelect = true; - selectKitOptions: { - none: "select_kit.default_header_text", - clearable: true, - filterable: true, - filterIcon: null, - closeOnChange: false, - autoInsertNoneItem: false, - headerComponent: "multi-select/multi-select-header", - filterComponent: "multi-select/multi-select-filter", - autoFilterable: true, - caretDownIcon: "caretIcon", - caretUpIcon: "caretIcon", - useHeaderFilter: false, - }, - - caretIcon: computed("value.[]", function () { + @computed("value.[]") + get caretIcon() { const maximum = this.selectKit.options.maximum; return maximum && makeArray(this.value).length >= parseInt(maximum, 10) ? null : "plus"; - }), + } search(filter) { - return this._super(filter).filter( - (content) => !makeArray(this.selectedContent).includes(content) - ); - }, + return super + .search(filter) + .filter((content) => !makeArray(this.selectedContent).includes(content)); + } append(values) { const existingItems = values @@ -60,7 +64,7 @@ export default SelectKitComponent.extend({ ); this.selectKit.change(newValues, newContent); - }, + } deselect(item) { this.clearErrors(); @@ -73,7 +77,7 @@ export default SelectKitComponent.extend({ this.valueProperty ? newContent.mapBy(this.valueProperty) : newContent, newContent ); - }, + } select(value, item) { if (this.selectKit.hasSelection && this.selectKit.options.maximum === 1) { @@ -122,42 +126,38 @@ export default SelectKitComponent.extend({ : makeArray(this.defaultItem(value, value)) ); } - }, + } - selectedContent: computed( - "value.[]", - "content.[]", - "selectKit.noneItem", - function () { - const value = makeArray(this.value).map((v) => - this.selectKit.options.castInteger && this._isNumeric(v) ? Number(v) : v - ); + @computed("value.[]", "content.[]", "selectKit.noneItem") + get selectedContent() { + const value = makeArray(this.value).map((v) => + this.selectKit.options.castInteger && this._isNumeric(v) ? Number(v) : v + ); - if (value.length) { - let content = []; + if (value.length) { + let content = []; - value.forEach((v) => { - if (this.selectKit.valueProperty) { - const c = makeArray(this.content).findBy( - this.selectKit.valueProperty, - v - ); - if (c) { - content.push(c); - } - } else { - if (makeArray(this.content).includes(v)) { - content.push(v); - } + value.forEach((v) => { + if (this.selectKit.valueProperty) { + const c = makeArray(this.content).findBy( + this.selectKit.valueProperty, + v + ); + if (c) { + content.push(c); } - }); + } else { + if (makeArray(this.content).includes(v)) { + content.push(v); + } + } + }); - return this.selectKit.modifySelection(content); - } - - return null; + return this.selectKit.modifySelection(content); } - ), + + return null; + } _onKeydown(event) { if ( @@ -192,5 +192,5 @@ export default SelectKitComponent.extend({ } return true; - }, -}); + } +} 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 d75b736f813..8ed7e069bd5 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -1,10 +1,14 @@ import Component from "@ember/component"; import EmberObject, { computed, get } from "@ember/object"; import { guidFor } from "@ember/object/internals"; -import Mixin from "@ember/object/mixin"; import { bind, cancel, next, schedule, throttle } from "@ember/runloop"; import { service } from "@ember/service"; import { isEmpty, isNone, isPresent } from "@ember/utils"; +import { + classNameBindings, + classNames, + tagName, +} from "@ember-decorators/component"; import { createPopper } from "@popperjs/core"; import { Promise } from "rsvp"; import { INPUT_DELAY } from "discourse-common/config/environment"; @@ -13,7 +17,7 @@ import deprecated from "discourse-common/lib/deprecated"; import { makeArray } from "discourse-common/lib/helpers"; import { bind as bindDecorator } from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; -import PluginApiMixin, { +import { applyContentPluginApiCallbacks, applyOnChangePluginApiCallbacks, } from "select-kit/mixins/plugin-api"; @@ -22,1232 +26,1257 @@ import UtilsMixin from "select-kit/mixins/utils"; export const MAIN_COLLECTION = "MAIN_COLLECTION"; export const ERRORS_COLLECTION = "ERRORS_COLLECTION"; -const EMPTY_OBJECT = Object.freeze({}); -const SELECT_KIT_OPTIONS = Mixin.create({ - concatenatedProperties: ["selectKitOptions"], - selectKitOptions: EMPTY_OBJECT, -}); - function isDocumentRTL() { return document.documentElement.classList.contains("rtl"); } -export default Component.extend( - SELECT_KIT_OPTIONS, - PluginApiMixin, - UtilsMixin, - { - tagName: "details", - pluginApiIdentifiers: ["select-kit"], - classNames: ["select-kit"], - classNameBindings: [ - "selectKit.isLoading:is-loading", - "selectKit.isExpanded:is-expanded", - "selectKit.options.disabled:is-disabled", - "selectKit.isHidden:is-hidden", - "selectKit.hasSelection:has-selection", - ], - tabindex: 0, - content: null, - value: null, - selectKit: null, - mainCollection: null, - errorsCollection: null, - options: null, - valueProperty: "id", - nameProperty: "name", - singleSelect: false, - multiSelect: false, - labelProperty: null, - titleProperty: null, - langProperty: null, - appEvents: service(), +/** + * Simulates the behavior of Ember's concatenatedProperties under native class syntax + */ +function concatProtoProperty(target, key, value) { + target.proto(); + target.prototype[key] = [ + ...makeArray(target.prototype[key]), + ...makeArray(value), + ]; +} - init() { - this._super(...arguments); +/** + * @decorator + * + * Apply select-kit options to a component class + * + */ +export function selectKitOptions(options) { + return function (target) { + concatProtoProperty(target, "selectKitOptions", options); + }; +} - this._searchPromise = null; +/** + * @decorator + * + * Register one or more plugin API identifiers for a component class + * + */ +export function pluginApiIdentifiers(identifiers) { + return function (target) { + concatProtoProperty(target, "pluginIdIdentifiers", identifiers); + }; +} - this.set("errorsCollection", []); - this._collections = [ERRORS_COLLECTION, MAIN_COLLECTION]; +// Decorator which converts a field into a prototype property. +// This allows the value to be overridden in subclasses, even if they're still +// using the legacy Ember `.extend()` syntax. +function protoProp(prototype, key, descriptor) { + return { + value: descriptor.initializer?.(), + writable: true, + enumerable: true, + configurable: true, + }; +} - !this.options && this.set("options", EmberObject.create({})); +@tagName("details") +@classNames("select-kit") +@classNameBindings( + "selectKit.isLoading:is-loading", + "selectKit.isExpanded:is-expanded", + "selectKit.options.disabled:is-disabled", + "selectKit.isHidden:is-hidden", + "selectKit.hasSelection:has-selection" +) +@selectKitOptions({ + allowAny: false, + showFullTitle: true, + none: null, + translatedNone: null, + filterable: false, + autoFilterable: "autoFilterable", + filterIcon: "search", + filterPlaceholder: null, + translatedFilterPlaceholder: null, + icon: null, + icons: null, + maximum: null, + maximumLabel: null, + minimum: null, + autoInsertNoneItem: true, + closeOnChange: true, + useHeaderFilter: false, + limitMatches: null, + placement: isDocumentRTL() ? "bottom-end" : "bottom-start", + verticalOffset: 3, + filterComponent: "select-kit/select-kit-filter", + selectedNameComponent: "selected-name", + selectedChoiceComponent: "selected-choice", + castInteger: false, + focusAfterOnChange: true, + triggerOnChangeOnTab: true, + autofocus: false, + placementStrategy: null, + mobilePlacementStrategy: null, + desktopPlacementStrategy: null, + hiddenValues: null, + disabled: false, + expandedOnInsert: false, + formName: null, +}) +@pluginApiIdentifiers(["select-kit"]) +export default class SelectKit extends Component.extend(UtilsMixin) { + @service appEvents; - this.handleDeprecations(); + singleSelect = false; + multiSelect = false; - this.set( - "selectKit", - EmberObject.create({ - uniqueID: this.id || guidFor(this), - valueProperty: this.valueProperty, - nameProperty: this.nameProperty, - labelProperty: this.labelProperty, - titleProperty: this.titleProperty, - langProperty: this.langProperty, - options: EmberObject.create(), + @protoProp tabindex = 0; + @protoProp content = null; + @protoProp value = null; + @protoProp selectKit = null; + @protoProp mainCollection = null; + @protoProp errorsCollection = null; + @protoProp options = null; + @protoProp valueProperty = "id"; + @protoProp nameProperty = "name"; + @protoProp labelProperty = null; + @protoProp titleProperty = null; + @protoProp langProperty = null; - isLoading: false, - isHidden: false, - isExpanded: false, - isFilterExpanded: false, - enterDisabled: false, - hasSelection: false, - hasNoContent: true, - highlighted: null, - noneItem: null, - newItem: null, - filter: null, + init() { + super.init(...arguments); - modifyContent: bind(this, this._modifyContentWrapper), - modifySelection: bind(this, this._modifySelectionWrapper), - modifyComponentForRow: bind(this, this._modifyComponentForRowWrapper), - modifyContentForCollection: bind( - this, - this._modifyContentForCollectionWrapper - ), - modifyComponentForCollection: bind( - this, - this._modifyComponentForCollectionWrapper - ), + this._searchPromise = null; - toggle: bind(this, this._toggle), - close: bind(this, this._close), - open: bind(this, this._open), - highlightNext: bind(this, this._highlightNext), - highlightPrevious: bind(this, this._highlightPrevious), - highlightLast: bind(this, this._highlightLast), - highlightFirst: bind(this, this._highlightFirst), - deselectLast: bind(this, this._deselectLast), - change: bind(this, this._onChangeWrapper), - select: bind(this, this.select), - deselect: bind(this, this.deselect), - deselectByValue: bind(this, this.deselectByValue), - append: bind(this, this.append), - cancelSearch: bind(this, this._cancelSearch), - triggerSearch: bind(this, this.triggerSearch), - focusFilter: bind(this, this._focusFilter), + this.set("errorsCollection", []); + this._collections = [ERRORS_COLLECTION, MAIN_COLLECTION]; - onOpen: bind(this, this._onOpenWrapper), - onClose: bind(this, this._onCloseWrapper), - onInput: bind(this, this._onInput), - onClearSelection: bind(this, this._onClearSelection), - onHover: bind(this, this._onHover), - onKeydown: bind(this, this._onKeydownWrapper), + !this.options && this.set("options", EmberObject.create({})); - mainElement: bind(this, this._mainElement), - headerElement: bind(this, this._headerElement), - bodyElement: bind(this, this._bodyElement), - }) - ); - }, + this.handleDeprecations(); - _modifyComponentForRowWrapper(collection, item) { - let component = this.modifyComponentForRow(collection, item); - return component || "select-kit/select-kit-row"; - }, + this.set( + "selectKit", + EmberObject.create({ + uniqueID: this.id || guidFor(this), + valueProperty: this.valueProperty, + nameProperty: this.nameProperty, + labelProperty: this.labelProperty, + titleProperty: this.titleProperty, + langProperty: this.langProperty, + options: EmberObject.create(), - modifyComponentForRow() {}, - - _modifyContentForCollectionWrapper(identifier) { - let collection = this.modifyContentForCollection(identifier); - - if (!collection) { - switch (identifier) { - case ERRORS_COLLECTION: - collection = this.errorsCollection; - break; - default: - collection = this.mainCollection; - break; - } - } - - return collection; - }, - - modifyContentForCollection() {}, - - _modifyComponentForCollectionWrapper(identifier) { - let component = this.modifyComponentForCollection(identifier); - - if (!component) { - switch (identifier) { - case ERRORS_COLLECTION: - component = "select-kit/errors-collection"; - break; - default: - component = "select-kit/select-kit-collection"; - break; - } - } - - return component; - }, - - modifyComponentForCollection() {}, - - didUpdateAttrs() { - this._super(...arguments); - - this.handleDeprecations(); - }, - - didInsertElement() { - this._super(...arguments); - - this.appEvents.on("keyboard-visibility-change", this, this._updatePopper); - - if (this.selectKit.options.expandedOnInsert) { - next(() => { - this._open(); - }); - } - }, - - click(event) { - event.preventDefault(); - event.stopPropagation(); - }, - - willDestroyElement() { - this._super(...arguments); - - this._cancelSearch(); - - this.appEvents.off( - "keyboard-visibility-change", - this, - this._updatePopper - ); - - if (this.popper) { - this.popper.destroy(); - this.popper = null; - } - }, - - didReceiveAttrs() { - this._super(...arguments); - - const deprecatedOptions = this._resolveDeprecatedOptions(); - const mergedOptions = Object.assign({}, ...this.selectKitOptions); - Object.keys(mergedOptions).forEach((key) => { - if (isPresent(this.options[key])) { - this.selectKit.options.set(key, this.options[key]); - return; - } - - if (isPresent(deprecatedOptions[`options.${key}`])) { - this.selectKit.options.set(key, deprecatedOptions[`options.${key}`]); - return; - } - - const value = mergedOptions[key]; - - if ( - key === "componentForRow" || - key === "contentForCollection" || - key === "componentForCollection" - ) { - if (typeof value === "string") { - this.selectKit.options.set(key, () => value); - } else { - this.selectKit.options.set(key, bind(this, value)); - } - - return; - } - - if ( - typeof value === "string" && - !value.includes(".") && - value in this - ) { - const computedValue = get(this, value); - if (typeof computedValue !== "function") { - this.selectKit.options.set(key, computedValue); - return; - } - } - - this.selectKit.options.set(key, value); - }); - - this.selectKit.setProperties({ - hasSelection: !isEmpty(this.value), - noneItem: this._modifyNoSelectionWrapper(), + isLoading: false, + isHidden: false, + isExpanded: false, + isFilterExpanded: false, + enterDisabled: false, + hasSelection: false, + hasNoContent: true, + highlighted: null, + noneItem: null, newItem: null, - }); + filter: null, - if (this.selectKit.isExpanded) { - this.triggerSearch(); - } + modifyContent: bind(this, this._modifyContentWrapper), + modifySelection: bind(this, this._modifySelectionWrapper), + modifyComponentForRow: bind(this, this._modifyComponentForRowWrapper), + modifyContentForCollection: bind( + this, + this._modifyContentForCollectionWrapper + ), + modifyComponentForCollection: bind( + this, + this._modifyComponentForCollectionWrapper + ), - if (this.computeContent) { - this._deprecated( - `The \`computeContent()\` function is deprecated pass a \`content\` attribute or define a \`content\` computed property in your component.` - ); + toggle: bind(this, this._toggle), + close: bind(this, this._close), + open: bind(this, this._open), + highlightNext: bind(this, this._highlightNext), + highlightPrevious: bind(this, this._highlightPrevious), + highlightLast: bind(this, this._highlightLast), + highlightFirst: bind(this, this._highlightFirst), + deselectLast: bind(this, this._deselectLast), + change: bind(this, this._onChangeWrapper), + select: bind(this, this.select), + deselect: bind(this, this.deselect), + deselectByValue: bind(this, this.deselectByValue), + append: bind(this, this.append), + cancelSearch: bind(this, this._cancelSearch), + triggerSearch: bind(this, this.triggerSearch), + focusFilter: bind(this, this._focusFilter), - this.set("content", this.computeContent()); - } - }, + onOpen: bind(this, this._onOpenWrapper), + onClose: bind(this, this._onCloseWrapper), + onInput: bind(this, this._onInput), + onClearSelection: bind(this, this._onClearSelection), + onHover: bind(this, this._onHover), + onKeydown: bind(this, this._onKeydownWrapper), - selectKitOptions: { - allowAny: false, - showFullTitle: true, - none: null, - translatedNone: null, - filterable: false, - autoFilterable: "autoFilterable", - filterIcon: "search", - filterPlaceholder: null, - translatedFilterPlaceholder: null, - icon: null, - icons: null, - maximum: null, - maximumLabel: null, - minimum: null, - autoInsertNoneItem: true, - closeOnChange: true, - useHeaderFilter: false, - limitMatches: null, - placement: isDocumentRTL() ? "bottom-end" : "bottom-start", - verticalOffset: 3, - filterComponent: "select-kit/select-kit-filter", - selectedNameComponent: "selected-name", - selectedChoiceComponent: "selected-choice", - castInteger: false, - focusAfterOnChange: true, - triggerOnChangeOnTab: true, - autofocus: false, - placementStrategy: null, - mobilePlacementStrategy: null, - desktopPlacementStrategy: null, - hiddenValues: null, - disabled: false, - expandedOnInsert: false, - formName: null, - }, + mainElement: bind(this, this._mainElement), + headerElement: bind(this, this._headerElement), + bodyElement: bind(this, this._bodyElement), + }) + ); + } - autoFilterable: computed("content.[]", "selectKit.filter", function () { - return ( - this.selectKit.filter && - this.options.autoFilterable && - this.content.length > 15 - ); - }), + _modifyComponentForRowWrapper(collection, item) { + let component = this.modifyComponentForRow(collection, item); + return component || "select-kit/select-kit-row"; + } - collections: computed( - "selectedContent.[]", - "mainCollection.[]", - "errorsCollection.[]", - function () { - return this._collections.map((identifier) => { - return { - identifier, - content: this.selectKit.modifyContentForCollection(identifier), - }; - }); - } - ), + modifyComponentForRow() {} - createContentFromInput(input) { - return input; - }, + _modifyContentForCollectionWrapper(identifier) { + let collection = this.modifyContentForCollection(identifier); - validateCreate(filter, content) { - this.clearErrors(); - - return ( - filter.length > 0 && - content && - !content.map((c) => this.getValue(c)).includes(filter) && - !makeArray(this.value).includes(filter) - ); - }, - - validateSelect() { - this.clearErrors(); - - const selection = makeArray(this.value); - - const maximum = this.selectKit.options.maximum; - if (maximum && selection.length >= maximum) { - const key = - this.selectKit.options.maximumLabel || - "select_kit.max_content_reached"; - this.addError(I18n.t(key, { count: maximum })); - return false; - } - - return true; - }, - - addError(error) { - if (!this.errorsCollection.includes(error)) { - this.errorsCollection.pushObject(error); - } - - this._safeAfterRender(() => this._updatePopper()); - }, - - clearErrors() { - if (!this.element || this.isDestroyed || this.isDestroying) { - return; - } - - this.set("errorsCollection", []); - }, - - prependCollection(identifier) { - this._collections.unshift(identifier); - }, - - appendCollection(identifier) { - this._collections.push(identifier); - }, - - insertCollectionAtIndex(identifier, index) { - this._collections.insertAt(index, identifier); - }, - - insertBeforeCollection(identifier, insertedIdentifier) { - const index = this._collections.indexOf(identifier); - this.insertCollectionAtIndex(insertedIdentifier, index - 1); - }, - - insertAfterCollection(identifier, insertedIdentifier) { - const index = this._collections.indexOf(identifier); - this.insertCollectionAtIndex(insertedIdentifier, index + 1); - }, - - _onInput(event) { - this._updatePopper(); - - if (this._searchPromise) { - cancel(this._searchPromise); - } - - this.selectKit.set("isLoading", true); - - discourseDebounce( - this, - this._debouncedInput, - event.target.value, - INPUT_DELAY - ); - }, - - _debouncedInput(filter) { - this.selectKit.set("filter", filter); - this.triggerSearch(filter); - }, - - _onChangeWrapper(value, items) { - this.selectKit.set("filter", null); - - return new Promise((resolve) => { - if ( - !this.selectKit.valueProperty && - this.selectKit.noneItem === value - ) { - value = null; - items = []; - } - - value = makeArray(value); - items = makeArray(items); - - if (this.multiSelect) { - items = items.filter( - (i) => - i !== this.newItem && - i !== this.noneItem && - this.getValue(i) !== null - ); - - if (this.selectKit.options.maximum === 1) { - value = value.slice(0, 1); - items = items.slice(0, 1); - } - } - - if (this.singleSelect) { - value = isPresent(value.firstObject) ? value.firstObject : null; - items = isPresent(items.firstObject) ? items.firstObject : null; - } - - this._boundaryActionHandler("onChange", value, items); - - applyOnChangePluginApiCallbacks(value, items, this); - - resolve(items); - }).finally(() => { - if (!this.isDestroying && !this.isDestroyed) { - if ( - this.selectKit.options.closeOnChange || - (isPresent(value) && this.selectKit.options.maximum === 1) - ) { - this.selectKit.close(event); - } - - if (this.selectKit.options.focusAfterOnChange) { - this._safeAfterRender(() => { - this._focusFilter(); - this._updatePopper(); - }); - } - } - }); - }, - - _modifyContentWrapper(content) { - content = this.modifyContent(content); - - return applyContentPluginApiCallbacks(content, this); - }, - - modifyContent(content) { - return content; - }, - - _modifyNoSelectionWrapper() { - return this.modifyNoSelection(); - }, - - modifyNoSelection() { - if (this.selectKit.options.translatedNone) { - return this.defaultItem(null, this.selectKit.options.translatedNone); - } - - let none = this.selectKit.options.none; - if (isNone(none) && !this.selectKit.options.allowAny) { - return null; - } - - if ( - isNone(none) && - this.selectKit.options.allowAny && - !this.selectKit.isExpanded - ) { - return null; - } - - let item; - switch (typeof none) { - case "string": - item = this.defaultItem(null, I18n.t(none)); + if (!collection) { + switch (identifier) { + case ERRORS_COLLECTION: + collection = this.errorsCollection; break; default: - item = none; + collection = this.mainCollection; + break; } + } - return item; - }, + return collection; + } - _modifySelectionWrapper(item) { - return this.modifySelection(item); - }, + modifyContentForCollection() {} - modifySelection(item) { - return item; - }, + _modifyComponentForCollectionWrapper(identifier) { + let component = this.modifyComponentForCollection(identifier); - _onKeydownWrapper(event) { - return this._boundaryActionHandler("onKeydown", event); - }, - - _mainElement() { - return document.querySelector(`#${this.selectKit.uniqueID}`); - }, - - _headerElement() { - return this.selectKit.mainElement().querySelector("summary"); - }, - - _bodyElement() { - return this.selectKit.mainElement().querySelector(".select-kit-body"); - }, - - _onHover(value, item) { - throttle(this, this._highlight, item, 25, true); - }, - - _highlight(item) { - this.selectKit.set("highlighted", item); - }, - - _boundaryActionHandler(actionName, ...params) { - if (!this.element || this.isDestroying || this.isDestroyed) { - return; + if (!component) { + switch (identifier) { + case ERRORS_COLLECTION: + component = "select-kit/errors-collection"; + break; + default: + component = "select-kit/select-kit-collection"; + break; } + } - let boundaryAction = true; + return component; + } - const privateActionName = `_${actionName}`; - const privateAction = get(this, privateActionName); - if (privateAction) { - boundaryAction = privateAction.call(this, ...params); - } + modifyComponentForCollection() {} - if (this.actions) { - const componentAction = get(this.actions, actionName); - if (boundaryAction && componentAction) { - boundaryAction = componentAction.call(this, ...params); - } - } + didUpdateAttrs() { + super.didUpdateAttrs(...arguments); - const action = get(this, actionName); - if (boundaryAction && action) { - boundaryAction = action.call(this, ...params); - } + this.handleDeprecations(); + } - return boundaryAction; - }, + didInsertElement() { + super.didInsertElement(...arguments); - deselect() { - this.clearErrors(); - this.selectKit.change(null, null); - }, + this.appEvents.on("keyboard-visibility-change", this, this._updatePopper); - deselectByValue(value) { - if (!value) { - return; - } - - const item = this.itemForValue(value, this.selectedContent); - this.deselect(item); - }, - - append() { - // do nothing on general case - }, - - search(filter) { - let content = this.content || []; - if (filter) { - filter = this._normalize(filter); - content = content.filter((c) => { - const name = this._normalize(this.getName(c)); - return name?.includes(filter); - }); - } - return content; - }, - - triggerSearch(filter) { - this._searchPromise && cancel(this._searchPromise); - - this._searchPromise = this._searchWrapper( - filter || this.selectKit.filter - ); - }, - - _searchWrapper(filter) { - if (this.isDestroyed || this.isDestroying) { - return Promise.resolve([]); - } - - this.clearErrors(); - this.setProperties({ - mainCollection: [], - "selectKit.isLoading": true, - "selectKit.enterDisabled": true, - }); - this._safeAfterRender(() => this._updatePopper()); - - let content = []; - - return Promise.resolve(this.search(filter)) - .then((result) => { - if (this.isDestroyed || this.isDestroying) { - return []; - } - - if (this.selectKit.options.maximum === 0) { - this.set("selectKit.isLoading", false); - this.set("selectKit.hasNoContent", false); - return []; - } - - content = content.concat(makeArray(result)); - content = this.selectKit.modifyContent(content).filter(Boolean); - - if (this.selectKit.valueProperty) { - content = content.uniqBy(this.selectKit.valueProperty); - } else { - content = content.uniq(); - } - - if (this.selectKit.options.limitMatches) { - content = content.slice(0, this.selectKit.options.limitMatches); - } - - const noneItem = this.selectKit.noneItem; - if ( - this.selectKit.options.allowAny && - filter && - this.getName(noneItem) !== filter - ) { - filter = this.createContentFromInput(filter); - if (this.validateCreate(filter, content)) { - this.selectKit.set("newItem", this.defaultItem(filter, filter)); - content.unshift(this.selectKit.newItem); - } - } - - const hasNoContent = isEmpty(content); - if ( - this.selectKit.hasSelection && - noneItem && - this.selectKit.options.autoInsertNoneItem - ) { - content.unshift(noneItem); - } - - this.set("mainCollection", content); - - this.selectKit.setProperties({ - highlighted: - this.singleSelect && this.value - ? this.itemForValue(this.value, this.mainCollection) - : isEmpty(this.selectKit.filter) - ? null - : this.mainCollection.firstObject, - isLoading: false, - hasNoContent, - }); - - this._safeAfterRender(() => { - if (this.selectKit.isExpanded) { - this._updatePopper(); - this._focusFilter(); - } - }); - }) - .finally(() => { - if (this.isDestroyed || this.isDestroying) { - return; - } - this.set("selectKit.enterDisabled", false); - }); - }, - - _safeAfterRender(fn) { + if (this.selectKit.options.expandedOnInsert) { next(() => { - schedule("afterRender", () => { - if (!this.element || this.isDestroyed || this.isDestroying) { - return; - } - - fn(); - }); + this._open(); }); - }, + } + } - _scrollToRow(rowItem, preventScroll = true) { - const value = this.getValue(rowItem); + click(event) { + event.preventDefault(); + event.stopPropagation(); + } - let rowContainer; - if (isPresent(value)) { - rowContainer = this.element.querySelector( - `.select-kit-row[data-value="${value}"]` - ); - } else { - rowContainer = this.element.querySelector(".select-kit-row.is-none"); - } + willDestroyElement() { + super.willDestroyElement(...arguments); - rowContainer?.focus({ preventScroll }); - }, + this._cancelSearch(); - _highlightLast() { - const highlighted = this.mainCollection.objectAt( - this.mainCollection.length - 1 - ); - if (highlighted) { - this._scrollToRow(highlighted, false); - this.set("selectKit.highlighted", highlighted); - } - }, + this.appEvents.off("keyboard-visibility-change", this, this._updatePopper); - _highlightFirst() { - const highlighted = this.mainCollection.objectAt(0); - if (highlighted) { - this._scrollToRow(highlighted, false); - this.set("selectKit.highlighted", highlighted); - } - }, + if (this.popper) { + this.popper.destroy(); + this.popper = null; + } + } - _highlightNext() { - let highlightedIndex = this.mainCollection.indexOf( - this.selectKit.highlighted - ); - const count = this.mainCollection.length; + didReceiveAttrs() { + super.didReceiveAttrs(...arguments); - if (highlightedIndex < count - 1) { - highlightedIndex = highlightedIndex + 1; - } else { - if (this.selectKit.isFilterExpanded) { - this._focusFilter(); - this.set("selectKit.highlighted", null); - return; - } else { - highlightedIndex = 0; - } - } - - const highlighted = this.mainCollection.objectAt(highlightedIndex); - if (highlighted) { - this._scrollToRow(highlighted, false); - this.set("selectKit.highlighted", highlighted); - } - }, - - _highlightPrevious() { - let highlightedIndex = this.mainCollection.indexOf( - this.selectKit.highlighted - ); - const count = this.mainCollection.length; - - if (highlightedIndex > 0) { - highlightedIndex = highlightedIndex - 1; - } else { - if (this.selectKit.isFilterExpanded) { - this._focusFilter(); - this.set("selectKit.highlighted", null); - return; - } else { - highlightedIndex = count - 1; - } - } - - const highlighted = this.mainCollection.objectAt(highlightedIndex); - if (highlighted) { - this._scrollToRow(highlighted, false); - this.set("selectKit.highlighted", highlighted); - } - }, - - _deselectLast() { - if (this.selectKit.hasSelection) { - this.deselectByValue(this.value[this.value.length - 1]); - } - }, - - select(value, item) { - if (!isPresent(value)) { - this._onClearSelection(); - } else { - const existingItem = this.findValue(this.mainCollection, item); - if (existingItem) { - if (!this.validateSelect(item)) { - return; - } - } - - this.selectKit.change(value, item || this.defaultItem(value, value)); - } - }, - - _onClearSelection() { - this.selectKit.change(null, null); - }, - - _onOpenWrapper() { - return this._boundaryActionHandler("onOpen"); - }, - - _cancelSearch() { - this._searchPromise && cancel(this._searchPromise); - }, - - _onCloseWrapper() { - this._cancelSearch(); - this.set("selectKit.highlighted", null); - - return this._boundaryActionHandler("onClose"); - }, - - _toggle(event) { - if (this.selectKit.isExpanded) { - this._close(event); - } else { - this._open(event); - } - }, - - _close(event) { - if (!this.selectKit.isExpanded) { + const deprecatedOptions = this._resolveDeprecatedOptions(); + const mergedOptions = Object.assign({}, ...this.selectKitOptions); + Object.keys(mergedOptions).forEach((key) => { + if (isPresent(this.options[key])) { + this.selectKit.options.set(key, this.options[key]); return; } - this.selectKit.mainElement().open = false; - - this.clearErrors(); - - const inModal = this.element.closest(".fixed-modal"); - if (inModal && this.site.mobileView) { - const modalBody = inModal.querySelector(".modal-body"); - modalBody.style = ""; - } - - this.selectKit.onClose(event); - - this.selectKit.setProperties({ - isExpanded: false, - filter: null, - }); - }, - - _open(event) { - if (this.selectKit.isExpanded) { + if (isPresent(deprecatedOptions[`options.${key}`])) { + this.selectKit.options.set(key, deprecatedOptions[`options.${key}`]); return; } - this.selectKit.mainElement().open = true; - this.clearErrors(); - this.selectKit.onOpen(event); + const value = mergedOptions[key]; - if (!this.popper) { - const inModal = this.element.closest(".fixed-modal .modal-body"); - const anchor = document.querySelector( - `#${this.selectKit.uniqueID}-header` - ); - const popper = document.querySelector( - `#${this.selectKit.uniqueID}-body` - ); - const strategy = this._computePlacementStrategy(); - - let bottomOffset = 0; - if (this.capabilities.isIOS) { - bottomOffset += - parseInt( - getComputedStyle(document.documentElement) - .getPropertyValue("--safe-area-inset-bottom") - .trim(), - 10 - ) || 0; - } - if (this.site.mobileView) { - bottomOffset += - parseInt( - getComputedStyle(document.documentElement) - .getPropertyValue("--footer-nav-height") - .trim(), - 10 - ) || 0; + if ( + key === "componentForRow" || + key === "contentForCollection" || + key === "componentForCollection" + ) { + if (typeof value === "string") { + this.selectKit.options.set(key, () => value); + } else { + this.selectKit.options.set(key, bind(this, value)); } - this.popper = createPopper(anchor, popper, { - eventsEnabled: false, - strategy, - placement: this.selectKit.options.placement, - modifiers: [ - { - name: "eventListeners", - options: { - resize: this.site.desktopView, - scroll: this.site.desktopView, - }, - }, - { - name: "flip", - enabled: !inModal, - options: { - padding: { - top: - parseInt( - document.documentElement.style.getPropertyValue( - "--header-offset" - ), - 10 - ) || 0, - bottom: bottomOffset, - }, - }, - }, - { - name: "offset", - options: { - offset: [0, this.selectKit.options.verticalOffset], - }, - }, - { - name: "applySmallScreenOffset", - enabled: window.innerWidth <= 450, - phase: "main", - fn({ state }) { - if (!inModal) { - let { x } = state.elements.reference.getBoundingClientRect(); - if (strategy === "fixed") { - state.modifiersData.popperOffsets.x = 0 + 10; - } else { - state.modifiersData.popperOffsets.x = -x + 10; - } - } - }, - }, - { - name: "applySmallScreenMaxWidth", - enabled: window.innerWidth <= 450, - phase: "beforeWrite", - fn: ({ state }) => { - if (inModal) { - const innerModal = document.querySelector( - ".fixed-modal div.modal-inner-container" - ); - - if (innerModal) { - if (this.multiSelect) { - state.styles.popper.width = `${this.element.offsetWidth}px`; - } else { - state.styles.popper.width = `${ - innerModal.clientWidth - 20 - }px`; - } - } - } else { - state.styles.popper.width = `${window.innerWidth - 20}px`; - } - }, - }, - { - name: "minWidth", - enabled: window.innerWidth > 450, - phase: "beforeWrite", - requires: ["computeStyles"], - fn: ({ state }) => { - state.styles.popper.minWidth = `${Math.max( - state.rects.reference.width, - 220 - )}px`; - }, - effect: ({ state }) => { - state.elements.popper.style.minWidth = `${Math.max( - state.elements.reference.offsetWidth, - 220 - )}px`; - }, - }, - { - name: "modalHeight", - enabled: !!(inModal && this.site.mobileView), - phase: "afterWrite", - fn: ({ state }) => { - inModal.style = ""; - inModal.style.height = - inModal.clientHeight + state.rects.popper.height + "px"; - }, - }, - ], - }); + return; } - this.selectKit.setProperties({ - isExpanded: true, - isFilterExpanded: - this.selectKit.options.filterable || this.selectKit.options.allowAny, - }); - - if (this.selectKit.options.useHeaderFilter) { - this._focusFilterInput(); + if (typeof value === "string" && !value.includes(".") && value in this) { + const computedValue = get(this, value); + if (typeof computedValue !== "function") { + this.selectKit.options.set(key, computedValue); + return; + } } + this.selectKit.options.set(key, value); + }); + + this.selectKit.setProperties({ + hasSelection: !isEmpty(this.value), + noneItem: this._modifyNoSelectionWrapper(), + newItem: null, + }); + + if (this.selectKit.isExpanded) { this.triggerSearch(); + } - this._safeAfterRender(() => { - this._focusFilter(); - this._scrollToCurrent(); - this._updatePopper(); + if (this.computeContent) { + this._deprecated( + `The \`computeContent()\` function is deprecated pass a \`content\` attribute or define a \`content\` computed property in your component.` + ); + + this.set("content", this.computeContent()); + } + } + + @computed("content.[]", "selectKit.filter") + get autoFilterable() { + return ( + this.selectKit.filter && + this.options.autoFilterable && + this.content.length > 15 + ); + } + + @computed("selectedContent.[]", "mainCollection.[]", "errorsCollection.[]") + get collections() { + return this._collections.map((identifier) => { + return { + identifier, + content: this.selectKit.modifyContentForCollection(identifier), + }; + }); + } + + createContentFromInput(input) { + return input; + } + + validateCreate(filter, content) { + this.clearErrors(); + + return ( + filter.length > 0 && + content && + !content.map((c) => this.getValue(c)).includes(filter) && + !makeArray(this.value).includes(filter) + ); + } + + validateSelect() { + this.clearErrors(); + + const selection = makeArray(this.value); + + const maximum = this.selectKit.options.maximum; + if (maximum && selection.length >= maximum) { + const key = + this.selectKit.options.maximumLabel || "select_kit.max_content_reached"; + this.addError(I18n.t(key, { count: maximum })); + return false; + } + + return true; + } + + addError(error) { + if (!this.errorsCollection.includes(error)) { + this.errorsCollection.pushObject(error); + } + + this._safeAfterRender(() => this._updatePopper()); + } + + clearErrors() { + if (!this.element || this.isDestroyed || this.isDestroying) { + return; + } + + this.set("errorsCollection", []); + } + + prependCollection(identifier) { + this._collections.unshift(identifier); + } + + appendCollection(identifier) { + this._collections.push(identifier); + } + + insertCollectionAtIndex(identifier, index) { + this._collections.insertAt(index, identifier); + } + + insertBeforeCollection(identifier, insertedIdentifier) { + const index = this._collections.indexOf(identifier); + this.insertCollectionAtIndex(insertedIdentifier, index - 1); + } + + insertAfterCollection(identifier, insertedIdentifier) { + const index = this._collections.indexOf(identifier); + this.insertCollectionAtIndex(insertedIdentifier, index + 1); + } + + _onInput(event) { + this._updatePopper(); + + if (this._searchPromise) { + cancel(this._searchPromise); + } + + this.selectKit.set("isLoading", true); + + discourseDebounce( + this, + this._debouncedInput, + event.target.value, + INPUT_DELAY + ); + } + + _debouncedInput(filter) { + this.selectKit.set("filter", filter); + this.triggerSearch(filter); + } + + _onChangeWrapper(value, items) { + this.selectKit.set("filter", null); + + return new Promise((resolve) => { + if (!this.selectKit.valueProperty && this.selectKit.noneItem === value) { + value = null; + items = []; + } + + value = makeArray(value); + items = makeArray(items); + + if (this.multiSelect) { + items = items.filter( + (i) => + i !== this.newItem && + i !== this.noneItem && + this.getValue(i) !== null + ); + + if (this.selectKit.options.maximum === 1) { + value = value.slice(0, 1); + items = items.slice(0, 1); + } + } + + if (this.singleSelect) { + value = isPresent(value.firstObject) ? value.firstObject : null; + items = isPresent(items.firstObject) ? items.firstObject : null; + } + + this._boundaryActionHandler("onChange", value, items); + + applyOnChangePluginApiCallbacks(value, items, this); + + resolve(items); + }).finally(() => { + if (!this.isDestroying && !this.isDestroyed) { + if ( + this.selectKit.options.closeOnChange || + (isPresent(value) && this.selectKit.options.maximum === 1) + ) { + this.selectKit.close(event); + } + + if (this.selectKit.options.focusAfterOnChange) { + this._safeAfterRender(() => { + this._focusFilter(); + this._updatePopper(); + }); + } + } + }); + } + + _modifyContentWrapper(content) { + content = this.modifyContent(content); + + return applyContentPluginApiCallbacks(content, this); + } + + modifyContent(content) { + return content; + } + + _modifyNoSelectionWrapper() { + return this.modifyNoSelection(); + } + + modifyNoSelection() { + if (this.selectKit.options.translatedNone) { + return this.defaultItem(null, this.selectKit.options.translatedNone); + } + + let none = this.selectKit.options.none; + if (isNone(none) && !this.selectKit.options.allowAny) { + return null; + } + + if ( + isNone(none) && + this.selectKit.options.allowAny && + !this.selectKit.isExpanded + ) { + return null; + } + + let item; + switch (typeof none) { + case "string": + item = this.defaultItem(null, I18n.t(none)); + break; + default: + item = none; + } + + return item; + } + + _modifySelectionWrapper(item) { + return this.modifySelection(item); + } + + modifySelection(item) { + return item; + } + + _onKeydownWrapper(event) { + return this._boundaryActionHandler("onKeydown", event); + } + + _mainElement() { + return document.querySelector(`#${this.selectKit.uniqueID}`); + } + + _headerElement() { + return this.selectKit.mainElement().querySelector("summary"); + } + + _bodyElement() { + return this.selectKit.mainElement().querySelector(".select-kit-body"); + } + + _onHover(value, item) { + throttle(this, this._highlight, item, 25, true); + } + + _highlight(item) { + this.selectKit.set("highlighted", item); + } + + _boundaryActionHandler(actionName, ...params) { + if (!this.element || this.isDestroying || this.isDestroyed) { + return; + } + + let boundaryAction = true; + + const privateActionName = `_${actionName}`; + const privateAction = get(this, privateActionName); + if (privateAction) { + boundaryAction = privateAction.call(this, ...params); + } + + if (this.actions) { + const componentAction = get(this.actions, actionName); + if (boundaryAction && componentAction) { + boundaryAction = componentAction.call(this, ...params); + } + } + + const action = get(this, actionName); + if (boundaryAction && action) { + boundaryAction = action.call(this, ...params); + } + + return boundaryAction; + } + + deselect() { + this.clearErrors(); + this.selectKit.change(null, null); + } + + deselectByValue(value) { + if (!value) { + return; + } + + const item = this.itemForValue(value, this.selectedContent); + this.deselect(item); + } + + append() { + // do nothing on general case + } + + search(filter) { + let content = this.content || []; + if (filter) { + filter = this._normalize(filter); + content = content.filter((c) => { + const name = this._normalize(this.getName(c)); + return name?.includes(filter); }); - }, + } + return content; + } - _scrollToCurrent() { - if (this.value && this.mainCollection) { - let highlighted; - if (this.valueProperty) { - highlighted = this.mainCollection.findBy( - this.valueProperty, - this.value - ); + triggerSearch(filter) { + this._searchPromise && cancel(this._searchPromise); + + this._searchPromise = this._searchWrapper(filter || this.selectKit.filter); + } + + _searchWrapper(filter) { + if (this.isDestroyed || this.isDestroying) { + return Promise.resolve([]); + } + + this.clearErrors(); + this.setProperties({ + mainCollection: [], + "selectKit.isLoading": true, + "selectKit.enterDisabled": true, + }); + this._safeAfterRender(() => this._updatePopper()); + + let content = []; + + return Promise.resolve(this.search(filter)) + .then((result) => { + if (this.isDestroyed || this.isDestroying) { + return []; + } + + if (this.selectKit.options.maximum === 0) { + this.set("selectKit.isLoading", false); + this.set("selectKit.hasNoContent", false); + return []; + } + + content = content.concat(makeArray(result)); + content = this.selectKit.modifyContent(content).filter(Boolean); + + if (this.selectKit.valueProperty) { + content = content.uniqBy(this.selectKit.valueProperty); } else { - const index = this.mainCollection.indexOf(this.value); - highlighted = this.mainCollection.objectAt(index); + content = content.uniq(); } - if (highlighted) { - this._scrollToRow(highlighted, false); - this.set("selectKit.highlighted", highlighted); + if (this.selectKit.options.limitMatches) { + content = content.slice(0, this.selectKit.options.limitMatches); } - } - }, - _focusFilter(forceHeader = false) { - if (!this.selectKit.mainElement()) { + const noneItem = this.selectKit.noneItem; + if ( + this.selectKit.options.allowAny && + filter && + this.getName(noneItem) !== filter + ) { + filter = this.createContentFromInput(filter); + if (this.validateCreate(filter, content)) { + this.selectKit.set("newItem", this.defaultItem(filter, filter)); + content.unshift(this.selectKit.newItem); + } + } + + const hasNoContent = isEmpty(content); + if ( + this.selectKit.hasSelection && + noneItem && + this.selectKit.options.autoInsertNoneItem + ) { + content.unshift(noneItem); + } + + this.set("mainCollection", content); + + this.selectKit.setProperties({ + highlighted: + this.singleSelect && this.value + ? this.itemForValue(this.value, this.mainCollection) + : isEmpty(this.selectKit.filter) + ? null + : this.mainCollection.firstObject, + isLoading: false, + hasNoContent, + }); + + this._safeAfterRender(() => { + if (this.selectKit.isExpanded) { + this._updatePopper(); + this._focusFilter(); + } + }); + }) + .finally(() => { + if (this.isDestroyed || this.isDestroying) { + return; + } + this.set("selectKit.enterDisabled", false); + }); + } + + _safeAfterRender(fn) { + next(() => { + schedule("afterRender", () => { + if (!this.element || this.isDestroyed || this.isDestroying) { + return; + } + + fn(); + }); + }); + } + + _scrollToRow(rowItem, preventScroll = true) { + const value = this.getValue(rowItem); + + let rowContainer; + if (isPresent(value)) { + rowContainer = this.element.querySelector( + `.select-kit-row[data-value="${value}"]` + ); + } else { + rowContainer = this.element.querySelector(".select-kit-row.is-none"); + } + + rowContainer?.focus({ preventScroll }); + } + + _highlightLast() { + const highlighted = this.mainCollection.objectAt( + this.mainCollection.length - 1 + ); + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); + } + } + + _highlightFirst() { + const highlighted = this.mainCollection.objectAt(0); + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); + } + } + + _highlightNext() { + let highlightedIndex = this.mainCollection.indexOf( + this.selectKit.highlighted + ); + const count = this.mainCollection.length; + + if (highlightedIndex < count - 1) { + highlightedIndex = highlightedIndex + 1; + } else { + if (this.selectKit.isFilterExpanded) { + this._focusFilter(); + this.set("selectKit.highlighted", null); return; + } else { + highlightedIndex = 0; + } + } + + const highlighted = this.mainCollection.objectAt(highlightedIndex); + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); + } + } + + _highlightPrevious() { + let highlightedIndex = this.mainCollection.indexOf( + this.selectKit.highlighted + ); + const count = this.mainCollection.length; + + if (highlightedIndex > 0) { + highlightedIndex = highlightedIndex - 1; + } else { + if (this.selectKit.isFilterExpanded) { + this._focusFilter(); + this.set("selectKit.highlighted", null); + return; + } else { + highlightedIndex = count - 1; + } + } + + const highlighted = this.mainCollection.objectAt(highlightedIndex); + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); + } + } + + _deselectLast() { + if (this.selectKit.hasSelection) { + this.deselectByValue(this.value[this.value.length - 1]); + } + } + + select(value, item) { + if (!isPresent(value)) { + this._onClearSelection(); + } else { + const existingItem = this.findValue(this.mainCollection, item); + if (existingItem) { + if (!this.validateSelect(item)) { + return; + } } - if (!this.selectKit.mainElement().open) { + this.selectKit.change(value, item || this.defaultItem(value, value)); + } + } + + _onClearSelection() { + this.selectKit.change(null, null); + } + + _onOpenWrapper() { + return this._boundaryActionHandler("onOpen"); + } + + _cancelSearch() { + this._searchPromise && cancel(this._searchPromise); + } + + _onCloseWrapper() { + this._cancelSearch(); + this.set("selectKit.highlighted", null); + + return this._boundaryActionHandler("onClose"); + } + + _toggle(event) { + if (this.selectKit.isExpanded) { + this._close(event); + } else { + this._open(event); + } + } + + _close(event) { + if (!this.selectKit.isExpanded) { + return; + } + + this.selectKit.mainElement().open = false; + + this.clearErrors(); + + const inModal = this.element.closest(".fixed-modal"); + if (inModal && this.site.mobileView) { + const modalBody = inModal.querySelector(".modal-body"); + modalBody.style = ""; + } + + this.selectKit.onClose(event); + + this.selectKit.setProperties({ + isExpanded: false, + filter: null, + }); + } + + _open(event) { + if (this.selectKit.isExpanded) { + return; + } + + this.selectKit.mainElement().open = true; + this.clearErrors(); + this.selectKit.onOpen(event); + + if (!this.popper) { + const inModal = this.element.closest(".fixed-modal .modal-body"); + const anchor = document.querySelector( + `#${this.selectKit.uniqueID}-header` + ); + const popper = document.querySelector(`#${this.selectKit.uniqueID}-body`); + const strategy = this._computePlacementStrategy(); + + let bottomOffset = 0; + if (this.capabilities.isIOS) { + bottomOffset += + parseInt( + getComputedStyle(document.documentElement) + .getPropertyValue("--safe-area-inset-bottom") + .trim(), + 10 + ) || 0; + } + if (this.site.mobileView) { + bottomOffset += + parseInt( + getComputedStyle(document.documentElement) + .getPropertyValue("--footer-nav-height") + .trim(), + 10 + ) || 0; + } + + this.popper = createPopper(anchor, popper, { + eventsEnabled: false, + strategy, + placement: this.selectKit.options.placement, + modifiers: [ + { + name: "eventListeners", + options: { + resize: this.site.desktopView, + scroll: this.site.desktopView, + }, + }, + { + name: "flip", + enabled: !inModal, + options: { + padding: { + top: + parseInt( + document.documentElement.style.getPropertyValue( + "--header-offset" + ), + 10 + ) || 0, + bottom: bottomOffset, + }, + }, + }, + { + name: "offset", + options: { + offset: [0, this.selectKit.options.verticalOffset], + }, + }, + { + name: "applySmallScreenOffset", + enabled: window.innerWidth <= 450, + phase: "main", + fn({ state }) { + if (!inModal) { + let { x } = state.elements.reference.getBoundingClientRect(); + if (strategy === "fixed") { + state.modifiersData.popperOffsets.x = 0 + 10; + } else { + state.modifiersData.popperOffsets.x = -x + 10; + } + } + }, + }, + { + name: "applySmallScreenMaxWidth", + enabled: window.innerWidth <= 450, + phase: "beforeWrite", + fn: ({ state }) => { + if (inModal) { + const innerModal = document.querySelector( + ".fixed-modal div.modal-inner-container" + ); + + if (innerModal) { + if (this.multiSelect) { + state.styles.popper.width = `${this.element.offsetWidth}px`; + } else { + state.styles.popper.width = `${ + innerModal.clientWidth - 20 + }px`; + } + } + } else { + state.styles.popper.width = `${window.innerWidth - 20}px`; + } + }, + }, + { + name: "minWidth", + enabled: window.innerWidth > 450, + phase: "beforeWrite", + requires: ["computeStyles"], + fn: ({ state }) => { + state.styles.popper.minWidth = `${Math.max( + state.rects.reference.width, + 220 + )}px`; + }, + effect: ({ state }) => { + state.elements.popper.style.minWidth = `${Math.max( + state.elements.reference.offsetWidth, + 220 + )}px`; + }, + }, + { + name: "modalHeight", + enabled: !!(inModal && this.site.mobileView), + phase: "afterWrite", + fn: ({ state }) => { + inModal.style = ""; + inModal.style.height = + inModal.clientHeight + state.rects.popper.height + "px"; + }, + }, + ], + }); + } + + this.selectKit.setProperties({ + isExpanded: true, + isFilterExpanded: + this.selectKit.options.filterable || this.selectKit.options.allowAny, + }); + + if (this.selectKit.options.useHeaderFilter) { + this._focusFilterInput(); + } + + this.triggerSearch(); + + this._safeAfterRender(() => { + this._focusFilter(); + this._scrollToCurrent(); + this._updatePopper(); + }); + } + + _scrollToCurrent() { + if (this.value && this.mainCollection) { + let highlighted; + if (this.valueProperty) { + highlighted = this.mainCollection.findBy( + this.valueProperty, + this.value + ); + } else { + const index = this.mainCollection.indexOf(this.value); + highlighted = this.mainCollection.objectAt(index); + } + + if (highlighted) { + this._scrollToRow(highlighted, false); + this.set("selectKit.highlighted", highlighted); + } + } + } + + _focusFilter(forceHeader = false) { + if (!this.selectKit.mainElement()) { + return; + } + + if (!this.selectKit.mainElement().open) { + const headerContainer = this.getHeader(); + headerContainer && headerContainer.focus({ preventScroll: true }); + return; + } + + // setting focus as early as possible is best for iOS + // because render/promise delays may cause keyboard not to show + if (!forceHeader) { + this._focusFilterInput(); + } + + this._safeAfterRender(() => { + const input = this.getFilterInput(); + if (!forceHeader && input) { + this._focusFilterInput(); + } else if (!this.selectKit.options.preventHeaderFocus) { const headerContainer = this.getHeader(); headerContainer && headerContainer.focus({ preventScroll: true }); - return; } - - // setting focus as early as possible is best for iOS - // because render/promise delays may cause keyboard not to show - if (!forceHeader) { - this._focusFilterInput(); - } - - this._safeAfterRender(() => { - const input = this.getFilterInput(); - if (!forceHeader && input) { - this._focusFilterInput(); - } else if (!this.selectKit.options.preventHeaderFocus) { - const headerContainer = this.getHeader(); - headerContainer && headerContainer.focus({ preventScroll: true }); - } - }); - }, - - _focusFilterInput() { - const input = this.getFilterInput(); - - if (input && document.activeElement !== input) { - input.focus({ preventScroll: true }); - - if (typeof input.selectionStart === "number") { - input.selectionStart = input.selectionEnd = input.value.length; - } - } - }, - - getFilterInput() { - return document.querySelector(`#${this.selectKit.uniqueID}-filter input`); - }, - - getHeader() { - return document.querySelector(`#${this.selectKit.uniqueID}-header`); - }, - - handleDeprecations() { - this._deprecateValueAttribute(); - this._deprecateMutations(); - this._handleDeprecatedArgs(); - }, - - @bindDecorator - _updatePopper() { - this.popper?.update?.(); - }, - - _computePlacementStrategy() { - let placementStrategy = this.selectKit.options.placementStrategy; - - if (placementStrategy) { - return placementStrategy; - } - - if (this.capabilities.isIpadOS || this.site.mobileView) { - placementStrategy = - this.selectKit.options.mobilePlacementStrategy || "absolute"; - } else { - placementStrategy = - this.selectKit.options.desktopPlacementStrategy || "fixed"; - } - - return placementStrategy; - }, - - _deprecated(text) { - deprecated(text, { - since: "v2.4.0", - dropFrom: "2.9.0.beta1", - id: "discourse.select-kit", - }); - }, - - _deprecateValueAttribute() { - if (this.valueAttribute || this.valueAttribute === null) { - this._deprecated( - "The `valueAttribute` is deprecated. Use `valueProperty` instead" - ); - - this.set("valueProperty", this.valueAttribute); - } - }, - - _deprecateMutations() { - this.actions ??= {}; - - if (!this.onChange && !this.actions.onChange) { - this._deprecated( - "Implicit mutation has been deprecated, please use `onChange` handler" - ); - - this.actions.onChange = - this.onSelect || - this.actions.onSelect || - ((value) => this.set("value", value)); - } - }, - - _resolveDeprecatedOptions() { - const migrations = { - allowAny: "options.allowAny", - allowCreate: "options.allowAny", - filterable: "options.filterable", - excludeCategoryId: "options.excludeCategoryId", - scopedCategoryId: "options.scopedCategoryId", - allowUncategorized: "options.allowUncategorized", - none: "options.none", - rootNone: "options.none", - disabled: "options.disabled", - isDisabled: "options.disabled", - rootNoneLabel: "options.none", - showFullTitle: "options.showFullTitle", - title: "options.translatedNone", - maximum: "options.maximum", - minimum: "options.minimum", - i18nPostfix: "options.i18nPostfix", - i18nPrefix: "options.i18nPrefix", - btnCustomClasses: "options.btnCustomClasses", - castInteger: "options.castInteger", - }; - - const resolvedDeprecations = {}; - - Object.keys(migrations).forEach((from) => { - const to = migrations[from]; - if (this.get(from) && !this.get(to)) { - this._deprecated( - `The \`${from}\` attribute is deprecated. Use \`${to}\` instead` - ); - - resolvedDeprecations[(to, this.get(from))]; - } - }); - - return resolvedDeprecations; - }, - - _handleDeprecatedArgs() { - const migrations = { - headerIcon: "icon", - onExpand: "onOpen", - onCollapse: "onClose", - }; - - Object.keys(migrations).forEach((from) => { - const to = migrations[from]; - if (this.get(from) && !this.get(to)) { - this._deprecated( - `The \`${from}\` attribute is deprecated. Use \`${to}\` instead` - ); - - this.set(to, this.get(from)); - } - }); - }, + }); } -); + + _focusFilterInput() { + const input = this.getFilterInput(); + + if (input && document.activeElement !== input) { + input.focus({ preventScroll: true }); + + if (typeof input.selectionStart === "number") { + input.selectionStart = input.selectionEnd = input.value.length; + } + } + } + + getFilterInput() { + return document.querySelector(`#${this.selectKit.uniqueID}-filter input`); + } + + getHeader() { + return document.querySelector(`#${this.selectKit.uniqueID}-header`); + } + + handleDeprecations() { + this._deprecateValueAttribute(); + this._deprecateMutations(); + this._handleDeprecatedArgs(); + } + + @bindDecorator + _updatePopper() { + this.popper?.update?.(); + } + + _computePlacementStrategy() { + let placementStrategy = this.selectKit.options.placementStrategy; + + if (placementStrategy) { + return placementStrategy; + } + + if (this.capabilities.isIpadOS || this.site.mobileView) { + placementStrategy = + this.selectKit.options.mobilePlacementStrategy || "absolute"; + } else { + placementStrategy = + this.selectKit.options.desktopPlacementStrategy || "fixed"; + } + + return placementStrategy; + } + + _deprecated(text) { + deprecated(text, { + since: "v2.4.0", + dropFrom: "2.9.0.beta1", + id: "discourse.select-kit", + }); + } + + _deprecateValueAttribute() { + if (this.valueAttribute || this.valueAttribute === null) { + this._deprecated( + "The `valueAttribute` is deprecated. Use `valueProperty` instead" + ); + + this.set("valueProperty", this.valueAttribute); + } + } + + _deprecateMutations() { + this.actions ??= {}; + + if (!this.onChange && !this.actions.onChange) { + this._deprecated( + "Implicit mutation has been deprecated, please use `onChange` handler" + ); + + this.actions.onChange = + this.onSelect || + this.actions.onSelect || + ((value) => this.set("value", value)); + } + } + + _resolveDeprecatedOptions() { + const migrations = { + allowAny: "options.allowAny", + allowCreate: "options.allowAny", + filterable: "options.filterable", + excludeCategoryId: "options.excludeCategoryId", + scopedCategoryId: "options.scopedCategoryId", + allowUncategorized: "options.allowUncategorized", + none: "options.none", + rootNone: "options.none", + disabled: "options.disabled", + isDisabled: "options.disabled", + rootNoneLabel: "options.none", + showFullTitle: "options.showFullTitle", + title: "options.translatedNone", + maximum: "options.maximum", + minimum: "options.minimum", + i18nPostfix: "options.i18nPostfix", + i18nPrefix: "options.i18nPrefix", + btnCustomClasses: "options.btnCustomClasses", + castInteger: "options.castInteger", + }; + + const resolvedDeprecations = {}; + + Object.keys(migrations).forEach((from) => { + const to = migrations[from]; + if (this.get(from) && !this.get(to)) { + this._deprecated( + `The \`${from}\` attribute is deprecated. Use \`${to}\` instead` + ); + + resolvedDeprecations[(to, this.get(from))]; + } + }); + + return resolvedDeprecations; + } + + _handleDeprecatedArgs() { + const migrations = { + headerIcon: "icon", + onExpand: "onOpen", + onCollapse: "onClose", + }; + + Object.keys(migrations).forEach((from) => { + const to = migrations[from]; + if (this.get(from) && !this.get(to)) { + this._deprecated( + `The \`${from}\` attribute is deprecated. Use \`${to}\` instead` + ); + + this.set(to, this.get(from)); + } + }); + } +} + +// Keep the concatenatedProperties behavior for legacy `.extend()` subclasses +SelectKit.prototype.concatenatedProperties = [ + ...SelectKit.prototype.concatenatedProperties, + "selectKitOptions", + "pluginApiIdentifiers", +]; diff --git a/app/assets/javascripts/select-kit/addon/components/single-select.js b/app/assets/javascripts/select-kit/addon/components/single-select.js index 733f66c0e71..a3f6916ee7d 100644 --- a/app/assets/javascripts/select-kit/addon/components/single-select.js +++ b/app/assets/javascripts/select-kit/addon/components/single-select.js @@ -1,46 +1,45 @@ import { computed } from "@ember/object"; import { isEmpty } from "@ember/utils"; -import SelectKitComponent from "select-kit/components/select-kit"; +import { classNames } from "@ember-decorators/component"; +import SelectKitComponent, { + pluginApiIdentifiers, + selectKitOptions, +} from "select-kit/components/select-kit"; -export default SelectKitComponent.extend({ - pluginApiIdentifiers: ["single-select"], - classNames: ["single-select"], - singleSelect: true, +@classNames("single-select") +@selectKitOptions({ + headerComponent: "select-kit/single-select-header", +}) +@pluginApiIdentifiers(["single-select"]) +export default class SingleSelect extends SelectKitComponent { + singleSelect = true; - selectKitOptions: { - headerComponent: "select-kit/single-select-header", - }, + @computed("value", "content.[]", "selectKit.noneItem") + get selectedContent() { + if (!isEmpty(this.value)) { + let content; - selectedContent: computed( - "value", - "content.[]", - "selectKit.noneItem", - function () { - if (!isEmpty(this.value)) { - let content; + const value = + this.selectKit.options.castInteger && this._isNumeric(this.value) + ? Number(this.value) + : this.value; - const value = - this.selectKit.options.castInteger && this._isNumeric(this.value) - ? Number(this.value) - : this.value; + if (this.selectKit.valueProperty) { + content = (this.content || []).findBy( + this.selectKit.valueProperty, + value + ); - if (this.selectKit.valueProperty) { - content = (this.content || []).findBy( - this.selectKit.valueProperty, - value - ); - - return this.selectKit.modifySelection( - content || this.defaultItem(value, value) - ); - } else { - return this.selectKit.modifySelection( - (this.content || []).filter((c) => c === value) - ); - } + return this.selectKit.modifySelection( + content || this.defaultItem(value, value) + ); } else { - return this.selectKit.noneItem; + return this.selectKit.modifySelection( + (this.content || []).filter((c) => c === value) + ); } + } else { + return this.selectKit.noneItem; } - ), -}); + } +} diff --git a/app/assets/javascripts/select-kit/addon/mixins/plugin-api.js b/app/assets/javascripts/select-kit/addon/mixins/plugin-api.js index f206221bdf0..34e797ac9bf 100644 --- a/app/assets/javascripts/select-kit/addon/mixins/plugin-api.js +++ b/app/assets/javascripts/select-kit/addon/mixins/plugin-api.js @@ -1,4 +1,3 @@ -import Mixin from "@ember/object/mixin"; import { isNone } from "@ember/utils"; import { makeArray } from "discourse-common/lib/helpers"; @@ -97,9 +96,3 @@ export function clearCallbacks() { _onChangeCallbacks = {}; _replaceContentCallbacks = {}; } - -const EMPTY_ARRAY = Object.freeze([]); -export default Mixin.create({ - concatenatedProperties: ["pluginApiIdentifiers"], - pluginApiIdentifiers: EMPTY_ARRAY, -});