2017-10-31 12:10:49 +03:00
|
|
|
/* global key */
|
|
|
|
import Component from '@ember/component';
|
|
|
|
import Ember from 'ember';
|
2020-05-20 16:55:41 +03:00
|
|
|
import classic from 'ember-classic-decorator';
|
2020-01-15 16:53:51 +03:00
|
|
|
import fallbackIfUndefined from '../utils/computed-fallback-if-undefined';
|
2018-03-13 14:17:29 +03:00
|
|
|
import {A, isArray} from '@ember/array';
|
2020-01-15 16:53:51 +03:00
|
|
|
import {action, computed, get} from '@ember/object';
|
2017-10-31 12:10:49 +03:00
|
|
|
import {
|
|
|
|
advanceSelectableOption,
|
|
|
|
defaultMatcher,
|
|
|
|
filterOptions
|
|
|
|
} from 'ember-power-select/utils/group-utils';
|
|
|
|
import {htmlSafe} from '@ember/string';
|
|
|
|
import {isBlank} from '@ember/utils';
|
2020-01-15 16:53:51 +03:00
|
|
|
import {tagName} from '@ember-decorators/component';
|
2017-10-31 12:10:49 +03:00
|
|
|
import {task} from 'ember-concurrency';
|
|
|
|
|
|
|
|
const {Handlebars} = Ember;
|
|
|
|
|
|
|
|
const BACKSPACE = 8;
|
|
|
|
const TAB = 9;
|
|
|
|
|
2020-05-20 16:55:41 +03:00
|
|
|
@classic
|
2020-01-15 16:53:51 +03:00
|
|
|
@tagName('')
|
|
|
|
class GhTokenInput extends Component {
|
2017-10-31 12:10:49 +03:00
|
|
|
// public attrs
|
2020-01-15 16:53:51 +03:00
|
|
|
@fallbackIfUndefined(true) allowCreation
|
|
|
|
@fallbackIfUndefined(false) closeOnSelect
|
|
|
|
@fallbackIfUndefined('name') labelField
|
|
|
|
@fallbackIfUndefined(defaultMatcher) matcher
|
|
|
|
@fallbackIfUndefined('name') searchField
|
|
|
|
@fallbackIfUndefined('gh-token-input/trigger') triggerComponent
|
|
|
|
@fallbackIfUndefined('power-select-vertical-collection-options') optionsComponent
|
|
|
|
|
|
|
|
@computed('options.[]', 'selected.[]')
|
|
|
|
get optionsWithoutSelected() {
|
2019-03-06 16:53:54 +03:00
|
|
|
return this.optionsWithoutSelectedTask.perform();
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
// actions -----------------------------------------------------------------
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
@action
|
|
|
|
handleKeydown(select, event) {
|
|
|
|
// On backspace with empty text, remove the last token but deviate
|
|
|
|
// from default behaviour by not updating search to match last token
|
|
|
|
if (event.keyCode === BACKSPACE && isBlank(event.target.value)) {
|
|
|
|
let lastSelection = select.selected[select.selected.length - 1];
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
if (lastSelection) {
|
|
|
|
this.onChange(select.selected.slice(0, -1), select);
|
|
|
|
select.actions.search('');
|
|
|
|
select.actions.open(event);
|
2017-10-31 12:10:49 +03:00
|
|
|
}
|
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
// prevent default
|
|
|
|
return false;
|
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
// Tab should work the same as Enter if there's a highlighted option
|
|
|
|
if (event.keyCode === TAB && !isBlank(event.target.value) && select.highlighted) {
|
|
|
|
if (!select.selected || select.selected.indexOf(select.highlighted) === -1) {
|
|
|
|
select.actions.choose(select.highlighted, event);
|
|
|
|
event.preventDefault(); // keep focus in search
|
|
|
|
return false;
|
2017-10-31 12:10:49 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
// fallback to default
|
|
|
|
return true;
|
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
@action
|
|
|
|
handleFocus() {
|
|
|
|
key.setScope('gh-token-input');
|
2018-03-13 14:17:29 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
if (this.onFocus) {
|
|
|
|
this.onFocus(...arguments);
|
2017-10-31 12:10:49 +03:00
|
|
|
}
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
@action
|
|
|
|
handleBlur() {
|
|
|
|
key.setScope('default');
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
if (this.onBlur) {
|
|
|
|
this.onBlur(...arguments);
|
2017-10-31 12:10:49 +03:00
|
|
|
}
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
@action
|
2017-10-31 12:10:49 +03:00
|
|
|
searchAndSuggest(term, select) {
|
2019-03-06 16:53:54 +03:00
|
|
|
return this.searchAndSuggestTask.perform(term, select);
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
@action
|
|
|
|
selectOrCreate(selection, select, keyboardEvent) {
|
|
|
|
// allow tokens to be created with spaces
|
|
|
|
if (keyboardEvent && keyboardEvent.code === 'Space') {
|
|
|
|
select.actions.search(`${select.searchText} `);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// guard against return being pressed when nothing is selected
|
|
|
|
if (!isArray(selection)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let suggestion = selection.find(option => option.__isSuggestion__);
|
|
|
|
|
|
|
|
if (suggestion) {
|
|
|
|
this.onCreate(suggestion.__value__, select);
|
|
|
|
} else {
|
|
|
|
this.onChange(selection, select);
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear select search
|
|
|
|
select.actions.search('');
|
|
|
|
}
|
|
|
|
|
|
|
|
// tasks -------------------------------------------------------------------
|
|
|
|
|
|
|
|
@task(function* () {
|
|
|
|
let options = yield this.options;
|
|
|
|
let selected = yield this.selected;
|
|
|
|
return options.filter(o => !selected.includes(o));
|
|
|
|
})
|
|
|
|
optionsWithoutSelectedTask;
|
|
|
|
|
|
|
|
@task(function* (term, select) {
|
2019-03-06 16:53:54 +03:00
|
|
|
let newOptions = (yield this.optionsWithoutSelected).toArray();
|
2017-10-31 12:10:49 +03:00
|
|
|
|
|
|
|
if (term.length === 0) {
|
|
|
|
return newOptions;
|
|
|
|
}
|
|
|
|
|
2019-03-06 16:53:54 +03:00
|
|
|
let searchAction = this.search;
|
2017-10-31 12:10:49 +03:00
|
|
|
if (searchAction) {
|
|
|
|
let results = yield searchAction(term, select);
|
|
|
|
|
|
|
|
if (results.toArray) {
|
|
|
|
results = results.toArray();
|
|
|
|
}
|
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
this._addCreateOption(term, results);
|
2017-10-31 12:10:49 +03:00
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
newOptions = this._filter(A(newOptions), term);
|
|
|
|
this._addCreateOption(term, newOptions);
|
2017-10-31 12:10:49 +03:00
|
|
|
|
|
|
|
return newOptions;
|
2020-01-15 16:53:51 +03:00
|
|
|
})
|
|
|
|
searchAndSuggestTask;
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
// internal ----------------------------------------------------------------
|
|
|
|
|
|
|
|
// always select the first item in the list that isn't the "Add x" option
|
|
|
|
defaultHighlighted(select) {
|
|
|
|
let {results} = select;
|
|
|
|
let option = advanceSelectableOption(results, undefined, 1);
|
|
|
|
|
|
|
|
if (results.length > 1 && option.__isSuggestion__) {
|
|
|
|
option = advanceSelectableOption(results, option, 1);
|
2018-01-02 16:54:02 +03:00
|
|
|
}
|
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
return option;
|
|
|
|
}
|
|
|
|
|
|
|
|
// private -----------------------------------------------------------------
|
|
|
|
|
|
|
|
_addCreateOption(term, options) {
|
|
|
|
if (this._shouldShowCreateOption(term, options)) {
|
|
|
|
options.unshift(this._buildSuggestionForTerm(term));
|
2018-03-13 14:17:29 +03:00
|
|
|
}
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2018-03-13 14:17:29 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
_shouldShowCreateOption(term, options) {
|
|
|
|
if (!this.allowCreation) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
if (this.showCreateWhen) {
|
|
|
|
return this.showCreateWhen(term, options);
|
2017-10-31 12:10:49 +03:00
|
|
|
} else {
|
2020-01-15 16:53:51 +03:00
|
|
|
return this._hideCreateOptionOnSameTerm(term, options);
|
2017-10-31 12:10:49 +03:00
|
|
|
}
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
_buildSuggestionForTerm(term) {
|
|
|
|
return {
|
|
|
|
__isSuggestion__: true,
|
|
|
|
__value__: term,
|
|
|
|
text: this._buildSuggestionLabel(term)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_hideCreateOptionOnSameTerm(term, options) {
|
|
|
|
let searchField = this.searchField;
|
|
|
|
let existingOption = options.findBy(searchField, term);
|
|
|
|
return !existingOption;
|
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
_filter(options, searchText) {
|
2017-10-31 12:10:49 +03:00
|
|
|
let matcher;
|
2019-03-06 16:53:54 +03:00
|
|
|
if (this.searchField) {
|
|
|
|
matcher = (option, text) => this.matcher(get(option, this.searchField), text);
|
2017-10-31 12:10:49 +03:00
|
|
|
} else {
|
|
|
|
matcher = (option, text) => this.matcher(option, text);
|
|
|
|
}
|
|
|
|
return filterOptions(options || [], searchText, matcher);
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
_buildSuggestionLabel(term) {
|
|
|
|
if (this.buildSuggestion) {
|
|
|
|
return this.buildSuggestion(term);
|
2017-10-31 12:10:49 +03:00
|
|
|
}
|
|
|
|
return htmlSafe(`Add <strong>"${Handlebars.Utils.escapeExpression(term)}"...</strong>`);
|
|
|
|
}
|
2020-01-15 16:53:51 +03:00
|
|
|
}
|
2017-10-31 12:10:49 +03:00
|
|
|
|
2020-01-15 16:53:51 +03:00
|
|
|
export default GhTokenInput;
|