mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-22 17:41:55 +08:00
Follow up to refactoring the generic utils mixin used in select-kit components in https://github.com/discourse/discourse/pull/32594. This PR follows a different approach as the util functions related to properties here aren't easily extracted without a major change to the interface due to the dependency on `this.selectKit`. These util functions are instead declared on the prototype with a class decorator which ensures the same behaviour is maintained without relying on a mixin. It's largely a lift-and-shift with some minor refactoring of the conditional logic to reduce nesting and improve readability of the functions.
1297 lines
33 KiB
JavaScript
Vendored
1297 lines
33 KiB
JavaScript
Vendored
import Component from "@ember/component";
|
|
import EmberObject, { computed, get } from "@ember/object";
|
|
import { guidFor } from "@ember/object/internals";
|
|
import { getOwner } from "@ember/owner";
|
|
import { bind, cancel, next, 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 discourseDebounce from "discourse/lib/debounce";
|
|
import { bind as bindDecorator } from "discourse/lib/decorators";
|
|
import deprecated from "discourse/lib/deprecated";
|
|
import { INPUT_DELAY } from "discourse/lib/environment";
|
|
import { makeArray } from "discourse/lib/helpers";
|
|
import { i18n } from "discourse-i18n";
|
|
import { normalize } from "select-kit/lib/input-utils";
|
|
import {
|
|
applyContentPluginApiCallbacks,
|
|
applyOnChangePluginApiCallbacks,
|
|
} from "select-kit/lib/plugin-api";
|
|
import selectKitPropUtils from "select-kit/lib/select-kit-prop-utils";
|
|
|
|
export const MAIN_COLLECTION = "MAIN_COLLECTION";
|
|
export const ERRORS_COLLECTION = "ERRORS_COLLECTION";
|
|
|
|
function isDocumentRTL() {
|
|
return document.documentElement.classList.contains("rtl");
|
|
}
|
|
|
|
/**
|
|
* 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),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @decorator
|
|
*
|
|
* Apply select-kit options to a component class
|
|
*
|
|
*/
|
|
export function selectKitOptions(options) {
|
|
return function (target) {
|
|
concatProtoProperty(target, "selectKitOptions", options);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @decorator
|
|
*
|
|
* Register one or more plugin API identifiers for a component class
|
|
*
|
|
*/
|
|
export function pluginApiIdentifiers(identifiers) {
|
|
return function (target) {
|
|
concatProtoProperty(target, "pluginApiIdentifiers", identifiers);
|
|
};
|
|
}
|
|
|
|
export function resolveComponent(context, component) {
|
|
const owner = getOwner(context);
|
|
|
|
if (typeof component === "string") {
|
|
return owner.resolveRegistration(`component:${component}`);
|
|
}
|
|
|
|
return component;
|
|
}
|
|
|
|
// 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,
|
|
};
|
|
}
|
|
|
|
@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: "magnifying-glass",
|
|
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"])
|
|
@selectKitPropUtils
|
|
export default class SelectKit extends Component {
|
|
@service appEvents;
|
|
|
|
singleSelect = false;
|
|
multiSelect = false;
|
|
|
|
@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;
|
|
|
|
init() {
|
|
super.init(...arguments);
|
|
|
|
this._searchPromise = null;
|
|
|
|
this.set("errorsCollection", []);
|
|
this._collections = [ERRORS_COLLECTION, MAIN_COLLECTION];
|
|
|
|
!this.options && this.set("options", EmberObject.create({}));
|
|
|
|
this.handleDeprecations();
|
|
|
|
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(),
|
|
|
|
isLoading: false,
|
|
isHidden: false,
|
|
isExpanded: false,
|
|
isFilterExpanded: false,
|
|
enterDisabled: false,
|
|
hasSelection: false,
|
|
hasNoContent: true,
|
|
highlighted: null,
|
|
noneItem: null,
|
|
newItem: null,
|
|
filter: null,
|
|
|
|
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
|
|
),
|
|
|
|
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),
|
|
|
|
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),
|
|
|
|
mainElement: bind(this, this._mainElement),
|
|
headerElement: bind(this, this._headerElement),
|
|
bodyElement: bind(this, this._bodyElement),
|
|
})
|
|
);
|
|
}
|
|
|
|
_modifyComponentForRowWrapper(collection, item) {
|
|
let component = this.modifyComponentForRow(collection, item);
|
|
return component || "select-kit/select-kit-row";
|
|
}
|
|
|
|
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() {
|
|
super.didUpdateAttrs(...arguments);
|
|
|
|
this.handleDeprecations();
|
|
}
|
|
|
|
didInsertElement() {
|
|
super.didInsertElement(...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() {
|
|
super.willDestroyElement(...arguments);
|
|
|
|
this._cancelSearch();
|
|
|
|
this.appEvents.off("keyboard-visibility-change", this, this._updatePopper);
|
|
|
|
if (this.popper) {
|
|
this.popper.destroy();
|
|
this.popper = null;
|
|
}
|
|
}
|
|
|
|
didReceiveAttrs() {
|
|
super.didReceiveAttrs(...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(),
|
|
newItem: null,
|
|
});
|
|
|
|
if (this.selectKit.isExpanded) {
|
|
this.triggerSearch();
|
|
}
|
|
|
|
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(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(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 theAction = get(this, actionName);
|
|
if (boundaryAction && theAction) {
|
|
boundaryAction = theAction.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;
|
|
}
|
|
|
|
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) {
|
|
next(() => {
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|
|
});
|
|
}
|
|
|
|
_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.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));
|
|
}
|
|
});
|
|
}
|
|
|
|
_normalize(input) {
|
|
return normalize(input);
|
|
}
|
|
}
|
|
|
|
// Keep the concatenatedProperties behavior for legacy `.extend()` subclasses
|
|
SelectKit.prototype.concatenatedProperties = [
|
|
...SelectKit.prototype.concatenatedProperties,
|
|
"selectKitOptions",
|
|
"pluginApiIdentifiers",
|
|
];
|