|
@@ -5,7 +5,7 @@ import SlimSelect from 'slim-select';
|
|
|
import { createToast } from '../../bs';
|
|
import { createToast } from '../../bs';
|
|
|
import { hasUrl, hasExclusions, isTrigger } from '../util';
|
|
import { hasUrl, hasExclusions, isTrigger } from '../util';
|
|
|
import { DynamicParamsMap } from './dynamicParams';
|
|
import { DynamicParamsMap } from './dynamicParams';
|
|
|
-import { isStaticParams } from './types';
|
|
|
|
|
|
|
+import { isStaticParams, isOption } from './types';
|
|
|
import {
|
|
import {
|
|
|
hasMore,
|
|
hasMore,
|
|
|
isTruthy,
|
|
isTruthy,
|
|
@@ -23,7 +23,7 @@ import type { Option } from 'slim-select/dist/data';
|
|
|
import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types';
|
|
import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types';
|
|
|
|
|
|
|
|
// Empty placeholder option.
|
|
// Empty placeholder option.
|
|
|
-const PLACEHOLDER = {
|
|
|
|
|
|
|
+const EMPTY_PLACEHOLDER = {
|
|
|
value: '',
|
|
value: '',
|
|
|
text: '',
|
|
text: '',
|
|
|
placeholder: true,
|
|
placeholder: true,
|
|
@@ -52,6 +52,18 @@ export class APISelect {
|
|
|
*/
|
|
*/
|
|
|
public readonly placeholder: string;
|
|
public readonly placeholder: string;
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Empty/placeholder option. Display text is optionally overridden via the `data-empty-option`
|
|
|
|
|
+ * attribute.
|
|
|
|
|
+ */
|
|
|
|
|
+ public readonly emptyOption: Option;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Null option. When `data-null-option` attribute is a string, the value is used to created an
|
|
|
|
|
+ * option of type `{text: '<value from data-null-option>': 'null'}`.
|
|
|
|
|
+ */
|
|
|
|
|
+ public readonly nullOption: Nullable<Option> = null;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Event that will initiate the API call to NetBox to load option data. By default, the trigger
|
|
* Event that will initiate the API call to NetBox to load option data. By default, the trigger
|
|
|
* is `'load'`, so data will be fetched when the element renders on the page.
|
|
* is `'load'`, so data will be fetched when the element renders on the page.
|
|
@@ -144,11 +156,6 @@ export class APISelect {
|
|
|
*/
|
|
*/
|
|
|
private preSorted: boolean = false;
|
|
private preSorted: boolean = false;
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * This instance's available options.
|
|
|
|
|
- */
|
|
|
|
|
- private _options: Option[] = [PLACEHOLDER];
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* Array of options values which should be considered disabled or static.
|
|
* Array of options values which should be considered disabled or static.
|
|
|
*/
|
|
*/
|
|
@@ -181,6 +188,24 @@ export class APISelect {
|
|
|
this.disabledOptions = this.getDisabledOptions();
|
|
this.disabledOptions = this.getDisabledOptions();
|
|
|
this.disabledAttributes = this.getDisabledAttributes();
|
|
this.disabledAttributes = this.getDisabledAttributes();
|
|
|
|
|
|
|
|
|
|
+ const emptyOption = base.getAttribute('data-empty-option');
|
|
|
|
|
+ if (isTruthy(emptyOption)) {
|
|
|
|
|
+ this.emptyOption = {
|
|
|
|
|
+ text: emptyOption,
|
|
|
|
|
+ value: '',
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.emptyOption = EMPTY_PLACEHOLDER;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nullOption = base.getAttribute('data-null-option');
|
|
|
|
|
+ if (isTruthy(nullOption)) {
|
|
|
|
|
+ this.nullOption = {
|
|
|
|
|
+ text: nullOption,
|
|
|
|
|
+ value: 'null',
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
this.slim = new SlimSelect({
|
|
this.slim = new SlimSelect({
|
|
|
select: this.base,
|
|
select: this.base,
|
|
|
allowDeselect: true,
|
|
allowDeselect: true,
|
|
@@ -265,7 +290,7 @@ export class APISelect {
|
|
|
* This instance's available options.
|
|
* This instance's available options.
|
|
|
*/
|
|
*/
|
|
|
private get options(): Option[] {
|
|
private get options(): Option[] {
|
|
|
- return this._options;
|
|
|
|
|
|
|
+ return this.slim.data.data.filter(isOption);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -275,28 +300,30 @@ export class APISelect {
|
|
|
*/
|
|
*/
|
|
|
private set options(optionsIn: Option[]) {
|
|
private set options(optionsIn: Option[]) {
|
|
|
let newOptions = optionsIn;
|
|
let newOptions = optionsIn;
|
|
|
|
|
+ // Ensure null option is present, if it exists.
|
|
|
|
|
+ if (this.nullOption !== null) {
|
|
|
|
|
+ newOptions = [this.nullOption, ...newOptions];
|
|
|
|
|
+ }
|
|
|
|
|
+ // Sort options unless this element is pre-sorted.
|
|
|
if (!this.preSorted) {
|
|
if (!this.preSorted) {
|
|
|
- newOptions = optionsIn.sort((a, b) => (a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1));
|
|
|
|
|
|
|
+ newOptions = newOptions.sort((a, b) =>
|
|
|
|
|
+ a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
// Deduplicate options each time they're set.
|
|
// Deduplicate options each time they're set.
|
|
|
- let deduplicated = uniqueByProperty(newOptions, 'value');
|
|
|
|
|
|
|
+ const deduplicated = uniqueByProperty(newOptions, 'value');
|
|
|
// Determine if the new options have a placeholder.
|
|
// Determine if the new options have a placeholder.
|
|
|
const hasPlaceholder = typeof deduplicated.find(o => o.value === '') !== 'undefined';
|
|
const hasPlaceholder = typeof deduplicated.find(o => o.value === '') !== 'undefined';
|
|
|
// Get the placeholder index (note: if there is no placeholder, the index will be `-1`).
|
|
// Get the placeholder index (note: if there is no placeholder, the index will be `-1`).
|
|
|
const placeholderIdx = deduplicated.findIndex(o => o.value === '');
|
|
const placeholderIdx = deduplicated.findIndex(o => o.value === '');
|
|
|
|
|
|
|
|
- if (hasPlaceholder && placeholderIdx < 0) {
|
|
|
|
|
- // If there is a placeholder but it is not the first element (due to sorting or other merge
|
|
|
|
|
- // issues), remove it from the options array and place it in front.
|
|
|
|
|
- deduplicated.splice(placeholderIdx);
|
|
|
|
|
- deduplicated = [PLACEHOLDER, ...deduplicated];
|
|
|
|
|
- }
|
|
|
|
|
- if (!hasPlaceholder) {
|
|
|
|
|
- // If there is no placeholder, add one to the front of the array.
|
|
|
|
|
- deduplicated = [PLACEHOLDER, ...deduplicated];
|
|
|
|
|
|
|
+ if (hasPlaceholder && placeholderIdx >= 0) {
|
|
|
|
|
+ // If there is an existing placeholder, replace it.
|
|
|
|
|
+ deduplicated[placeholderIdx] = this.emptyOption;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // If there is not a placeholder, add one to the front.
|
|
|
|
|
+ deduplicated.unshift(this.emptyOption);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- this._options = deduplicated;
|
|
|
|
|
this.slim.setData(deduplicated);
|
|
this.slim.setData(deduplicated);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -304,7 +331,7 @@ export class APISelect {
|
|
|
* Remove all options and reset back to the generic placeholder.
|
|
* Remove all options and reset back to the generic placeholder.
|
|
|
*/
|
|
*/
|
|
|
private resetOptions(): void {
|
|
private resetOptions(): void {
|
|
|
- this.options = [PLACEHOLDER];
|
|
|
|
|
|
|
+ this.options = [this.emptyOption];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -348,7 +375,12 @@ export class APISelect {
|
|
|
const fetcher = debounce((event: Event) => this.handleSearch(event), 300, false);
|
|
const fetcher = debounce((event: Event) => this.handleSearch(event), 300, false);
|
|
|
|
|
|
|
|
// Query the API when the input value changes or a value is pasted.
|
|
// Query the API when the input value changes or a value is pasted.
|
|
|
- this.slim.slim.search.input.addEventListener('keyup', event => fetcher(event));
|
|
|
|
|
|
|
+ this.slim.slim.search.input.addEventListener('keyup', event => {
|
|
|
|
|
+ // Only search when necessary keys are pressed.
|
|
|
|
|
+ if (!event.key.match(/^(Arrow|Enter|Tab).*/)) {
|
|
|
|
|
+ return fetcher(event);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
this.slim.slim.search.input.addEventListener('paste', event => fetcher(event));
|
|
this.slim.slim.search.input.addEventListener('paste', event => fetcher(event));
|
|
|
|
|
|
|
|
// Watch every scroll event to determine if the scroll position is at bottom.
|
|
// Watch every scroll event to determine if the scroll position is at bottom.
|
|
@@ -437,7 +469,7 @@ export class APISelect {
|
|
|
for (const result of data.results) {
|
|
for (const result of data.results) {
|
|
|
let text = result.display;
|
|
let text = result.display;
|
|
|
|
|
|
|
|
- if (typeof result._depth === 'number') {
|
|
|
|
|
|
|
+ if (typeof result._depth === 'number' && result._depth > 0) {
|
|
|
// If the object has a `_depth` property, indent its display text.
|
|
// If the object has a `_depth` property, indent its display text.
|
|
|
if (!this.preSorted) {
|
|
if (!this.preSorted) {
|
|
|
this.preSorted = true;
|
|
this.preSorted = true;
|
|
@@ -534,7 +566,7 @@ export class APISelect {
|
|
|
*/
|
|
*/
|
|
|
private async getOptions(action: ApplyMethod = 'merge'): Promise<void> {
|
|
private async getOptions(action: ApplyMethod = 'merge'): Promise<void> {
|
|
|
if (this.queryUrl.includes(`{{`)) {
|
|
if (this.queryUrl.includes(`{{`)) {
|
|
|
- this.options = [PLACEHOLDER];
|
|
|
|
|
|
|
+ this.resetOptions();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
await this.fetchOptions(this.queryUrl, action);
|
|
await this.fetchOptions(this.queryUrl, action);
|