"use strict";
define(["backbone", "collections/searchSelect/SearchSelectOptions"], (
Backbone,
SearchSelectOptions,
) => {
/**
* @class SearchSelect
* @classdesc A model for managing dropdown options and state for a search
* select component.
* @classcategory Models/SearchSelect
* @since 2.31.0
* @augments Backbone.Model
*/
const SearchSelect = Backbone.Model.extend({
/** @lends SearchSelect.prototype */
/**
* @returns {object} Default attributes for a SearchSelect model.
* @property {boolean} allowMulti - Whether to allow users to select more
* than one value.
* @property {boolean} allowAdditions - Allows users to add their own
* options not listed in options.
* @property {boolean} clearable - Whether the dropdown can be cleared by
* the user after selection.
* @property {string} submenuStyle - Determines the display style of items
* in categories ("list", "popout", "accordion").
* @property {boolean} hideEmptyCategoriesOnSearch - Displays category
* headers in the dropdown even with no results.
* @property {SearchSelectOptions} options - Collection of
* SearchSelectOption models that represent choices a user can select from.
* @property {string[]} selected - Currently selected values in the
* dropdown.
* @property {string[]|boolean} separatorOptions - For select inputs where
* multiple values are allowed (allowMulti is true), a list of options that
* can be used as separators between values. To turn off this feature, set
* to false or an empty array.
* @property {string} separator - The current separator to use between
* selected values, must be one of the separatorOptions.
* @property {string} searchTerm - The current search term being used to
* filter options, if any. This will be updated by the view.
* @property {string} originalSubmenuStyle - A reference to the original
* submenu style since the submenu style can change during search. This will
* be set automatically by the model during initialization.
* @property {object|boolean} apiSettings - Settings for retrieving data via
* API, false if not using remote content.
* @see
* {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings}
* @see {@link https://fomantic-ui.com/behaviors/api.html#/settings}
* @property {string} placeholderText Text to show in the input field before
* any value has been entered.
* @property {string} inputLabel Label for the input element.
* @property {boolean} buttonStyle Set this to true to render the dropdown
* as more of a button-like interface. This works best for single-select
* dropdowns.
* @property {string|boolean} icon Set this to a FontAwesome icon to use
* instead of the default dropdown (down arrow) icon. Works will with the
* buttonStyle option.
* @property {boolean} fluid Set this to true to make the dropdown take up
* the full width of its container.
* @property {boolean} compact Set this to true to make the dropdown more
* compact, e.g. for the filter bar in the catalog search.
*/
defaults() {
return {
allowMulti: true,
allowAdditions: false,
clearable: true,
submenuStyle: "list",
hideEmptyCategoriesOnSearch: true,
options: new SearchSelectOptions(),
selected: [],
separatorOptions: ["AND", "OR"],
separator: "",
searchTerm: "",
originalSubmenuStyle: "",
apiSettings: false,
placeholderText: "Search for or select a value",
inputLabel: "Select a value",
buttonStyle: false,
icon: false,
fluid: true,
compact: false,
};
},
/** @inheritdoc */
initialize(attributes, _options) {
this.setOptionsForPreselected();
const optionsData = attributes?.options;
// Select options must be parsed if they are not already
// SearchSelectOption collections
if (optionsData && !(optionsData instanceof SearchSelectOptions)) {
this.set(
"options",
new SearchSelectOptions(optionsData, { parse: true }),
);
}
// Save a reference to the original submenu style to revert to when search
// term is removed
const originalSubmenuStyle = this.get("submenuStyle");
this.set("originalSubmenuStyle", originalSubmenuStyle);
// Set a listener to change the submenu style when a user is searching
if (originalSubmenuStyle !== "list") {
this.changeSubmenuOnSearch();
}
},
/**
* Set a listener to change the current submenu style to "list" when a
* search term is present, and revert to the original style when the search
* term is removed. This is to ensure that the dropdown displays the list
* style with only the search results when a user is searching. This is only
* necessary if the submenu style is not already set to "list".
*/
changeSubmenuOnSearch() {
this.listenTo(this, "change:searchTerm", (model, searchTerm) => {
const originalSubmenuStyle = this.get("originalSubmenuStyle");
const submenuStyle = searchTerm ? "list" : originalSubmenuStyle;
model.set("submenuStyle", submenuStyle);
});
},
/**
* Update the options for the dropdown.
* @param {object|object[]} options The new options to set for the dropdown
*/
updateOptions(options) {
const parse = typeof options === "object" && !Array.isArray(options);
this.get("options").reset(options, { parse });
},
/**
* Returns the options as a JSON object.
* @param {boolean} categorized - Whether to return the options as
* categorized. See @link{SearchSelectOptions#toJSON} for more information.
* @returns {object|object[]} - The options as a JSON object.
*/
optionsAsJSON(categorized = false) {
return this.get("options").toJSON(categorized);
},
/**
* Checks if a value is one of the values in the options.
* @param {string} value - The value to check for.
* @returns {boolean} - Returns true if the value is found in the
* collection, false otherwise.
*/
isValidValue(value) {
if (this.get("allowAdditions")) return true;
return this.get("options").isValidValue(value);
},
/**
* Check if there are any invalid selections in the selected values.
* @returns {boolean | string[]} - Returns false if there are no invalid
* selections, or an array of invalid selection strings if there are any.
*/
hasInvalidSelections() {
if (this.get("allowAdditions")) return false;
const selected = this.get("selected");
if (!selected || !selected.length) return false;
const invalidSelections = selected.filter(
(value) => !this.isValidValue(value),
);
return invalidSelections.length ? invalidSelections : false;
},
/**
* Add a selected value and ensures it's not already in the list. If this is
* not a multi-select dropdown, the selected value will replace any existing
* value.
* @param {string} value - The value to add to the selected list.
* @param {object} options - Additional options to be passed to the set
* method.
*/
addSelected(value, options = {}) {
const selected = this.get("selected");
if (selected.includes(value)) return;
const newSelected = this.get("allowMulti")
? [...selected, value]
: [value];
this.setSelected(newSelected, options);
},
/**
* Change the values that are selected in the dropdown.
* @param {string|string[]} values - The value(s) to select.
* @param {object} options - Additional options to be passed to the set
* method.
*/
setSelected(values, options = {}) {
const newValues = !Array.isArray(values) ? [values] : values;
const selected = [...newValues];
this.set({ selected }, options);
},
/**
* Remove a value from the selected list.
* @param {string} value - The value to remove from the selected list.
* @param {object} options - Additional options to be passed to the set
* method.
*/
removeSelected(value, options = {}) {
const selected = this.get("selected");
const newSelected = selected.filter((val) => val !== value);
this.set({ selected: newSelected }, options);
},
/**
* Determines if a separator is needed for the newly created, yet to be attached label.
* @param {string} value - The value of the label.
* @returns {boolean} - Returns true if a separator should be created, otherwise false.
*/
separatorRequired(value) {
// must have at least a current separator
if (!this.get("separator")) return false;
const selected = this.get("selected");
return (
this.get("allowMulti") && selected?.length > 1 && selected[0] !== value
);
},
/**
* Checks if it's possible to update the separator that is used between
* selected values. For this to be possible, there must be more than one
* separator option available.
* @returns {boolean} - Returns true if the separator can be changed, false
* otherwise.
*/
canChangeSeparator() {
return (
this.get("separatorOptions") && this.get("separatorOptions").length > 1
);
},
/**
* Get the next separator in the list of separator options.
* @returns {string|null} - The next separator in the list of separator
* options, or null if none.
*/
getNextSeparator() {
const separators = this.get("separatorOptions");
const currentSeparator = this.get("separator");
if (!currentSeparator || !separators || !separators.length) return null;
const currentIndex = separators.indexOf(currentSeparator);
let nextIndex = currentIndex + 1;
if (nextIndex >= separators.length) {
nextIndex = 0;
}
return separators[nextIndex];
},
/**
* Set the next separator in the list of separator options.
*/
setNextSeparator() {
const nextSeparator = this.getNextSeparator();
if (!nextSeparator) return;
this.set("separator", nextSeparator);
},
/**
* Get the selected models from the options collection.
* @returns {SearchSelectOption[]} - The selected models from the options
* collection.
*/
getSelectedModels() {
const selected = this.get("selected");
return this.get("options").filter((model) =>
selected.includes(model.get("value")),
);
},
/**
* This method is set for extended models that fetch options asynchronously
* on search. This method should be overridden to fetch options from the API
* for the values that are currently selected in the dropdown. This allows
* those values to be populated with the correct label and icon.
*/
setOptionsForPreselected() {
// This method should be overridden in extended models
},
});
return SearchSelect;
});