mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 06:23:43 +08:00
a914d3230b
Followup to 7d8974d02f
Co-authored-by: David Taylor <david@taylorhq.com>
1283 lines
33 KiB
JavaScript
1283 lines
33 KiB
JavaScript
import Component from "@ember/component";
|
|
import EmberObject, { computed, get } from "@ember/object";
|
|
import { guidFor } from "@ember/object/internals";
|
|
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";
|
|
import discourseDebounce from "discourse-common/lib/debounce";
|
|
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 {
|
|
applyContentPluginApiCallbacks,
|
|
applyOnChangePluginApiCallbacks,
|
|
} from "select-kit/mixins/plugin-api";
|
|
import UtilsMixin from "select-kit/mixins/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);
|
|
};
|
|
}
|
|
|
|
// 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"])
|
|
export default class SelectKit extends Component.extend(UtilsMixin) {
|
|
@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.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;
|
|
}
|
|
|
|
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(() => {
|
|
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;
|
|
}
|
|
}
|
|
|
|
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.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",
|
|
];
|