Commit e994dea0 authored by Andrew Nicols's avatar Andrew Nicols
Browse files

MDL-62514 behat: Rewrite handling of autocomplete

This includes a minor restructure of the autocomplete JS to make use of
promises and improve tracking of pending JS.

In particular it improves the way in which throttled text input is
handled to ensure that the behat does not continue until:
- typing is fully complete; and
- all possible ajax requests have been sent; and
- all possible ajax requests complete; and
- the suggestions are updated.

A number of conditions existed where behat would move on to the next
step too early in a race condition effect between Behat and Autocomplete.
parent 7bfb575f
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
...@@ -46,6 +46,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -46,6 +46,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @private * @private
* @param {Number} index The index in the current (visible) list of selection. * @param {Number} index The index in the current (visible) list of selection.
* @param {Object} state State variables for this autocomplete element. * @param {Object} state State variables for this autocomplete element.
* @return {Promise}
*/ */
var activateSelection = function(index, state) { var activateSelection = function(index, state) {
// Find the elements in the DOM. // Find the elements in the DOM.
...@@ -69,6 +70,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -69,6 +70,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
element.attr('data-active-selection', true).attr('id', itemId); element.attr('data-active-selection', true).attr('id', itemId);
// Tell the input field it has a new active descendant so the item is announced. // Tell the input field it has a new active descendant so the item is announced.
selectionElement.attr('aria-activedescendant', itemId); selectionElement.attr('aria-activedescendant', itemId);
return $.Deferred().resolve();
}; };
/** /**
...@@ -79,8 +82,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -79,8 +82,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} options Original options for this autocomplete element. * @param {Object} options Original options for this autocomplete element.
* @param {Object} state State variables for this autocomplete element. * @param {Object} state State variables for this autocomplete element.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list. * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @return {Promise}
*/ */
var updateSelectionList = function(options, state, originalSelect) { var updateSelectionList = function(options, state, originalSelect) {
var pendingKey = 'form-autocomplete-updateSelectionList-' + state.inputId;
M.util.js_pending(pendingKey);
// Build up a valid context to re-render the template. // Build up a valid context to re-render the template.
var items = []; var items = [];
var newSelection = $(document.getElementById(state.selectionId)); var newSelection = $(document.getElementById(state.selectionId));
...@@ -104,9 +111,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -104,9 +111,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
var context = $.extend({items: items}, options, state); var context = $.extend({items: items}, options, state);
// Render the template. // Render the template.
templates.render('core/form_autocomplete_selection', context).done(function(newHTML) { return templates.render('core/form_autocomplete_selection', context)
.then(function(html, js) {
// Add it to the page. // Add it to the page.
newSelection.empty().append($(newHTML).html()); templates.replaceNodeContents(newSelection, html, js);
if (activeValue !== false) { if (activeValue !== false) {
// Reselect any previously selected item. // Reselect any previously selected item.
...@@ -116,7 +124,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -116,7 +124,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
} }
}); });
} }
}).fail(notification.exception);
return activeValue;
})
.then(function() {
return M.util.js_complete(pendingKey);
})
.catch(notification.exception);
}; };
/** /**
...@@ -140,6 +154,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -140,6 +154,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} state State variables for this autocomplete element. * @param {Object} state State variables for this autocomplete element.
* @param {Element} item The item to be deselected. * @param {Element} item The item to be deselected.
* @param {Element} originalSelect The original select list. * @param {Element} originalSelect The original select list.
* @return {Promise}
*/ */
var deselectItem = function(options, state, item, originalSelect) { var deselectItem = function(options, state, item, originalSelect) {
var selectedItemValue = $(item).attr('data-value'); var selectedItemValue = $(item).attr('data-value');
...@@ -158,9 +173,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -158,9 +173,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
}); });
} }
// Rerender the selection list. // Rerender the selection list.
updateSelectionList(options, state, originalSelect); return updateSelectionList(options, state, originalSelect)
// Notifiy that the selection changed. .then(function() {
// Notify that the selection changed.
notifyChange(originalSelect); notifyChange(originalSelect);
return;
});
}; };
/** /**
...@@ -170,6 +189,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -170,6 +189,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @private * @private
* @param {Number} index The index in the current (visible) list of suggestions. * @param {Number} index The index in the current (visible) list of suggestions.
* @param {Object} state State variables for this instance of autocomplete. * @param {Object} state State variables for this instance of autocomplete.
* @return {Promise}
*/ */
var activateItem = function(index, state) { var activateItem = function(index, state) {
// Find the elements in the DOM. // Find the elements in the DOM.
...@@ -202,9 +222,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -202,9 +222,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
- suggestionsElement.offset().top - suggestionsElement.offset().top
+ suggestionsElement.scrollTop() + suggestionsElement.scrollTop()
- (suggestionsElement.height() / 2); - (suggestionsElement.height() / 2);
suggestionsElement.animate({ return suggestionsElement.animate({
scrollTop: scrollPos scrollTop: scrollPos
}, 100); }, 100).promise();
}; };
/** /**
...@@ -213,6 +233,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -213,6 +233,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method activateNextItem * @method activateNextItem
* @private * @private
* @param {Object} state State variable for this auto complete element. * @param {Object} state State variable for this auto complete element.
* @return {Promise}
*/ */
var activateNextItem = function(state) { var activateNextItem = function(state) {
// Find the list of suggestions. // Find the list of suggestions.
...@@ -222,7 +243,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -222,7 +243,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Find it's index. // Find it's index.
var current = suggestionsElement.children('[aria-hidden=false]').index(element); var current = suggestionsElement.children('[aria-hidden=false]').index(element);
// Activate the next one. // Activate the next one.
activateItem(current + 1, state); return activateItem(current + 1, state);
}; };
/** /**
...@@ -231,6 +252,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -231,6 +252,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method activatePreviousSelection * @method activatePreviousSelection
* @private * @private
* @param {Object} state State variables for this instance of autocomplete. * @param {Object} state State variables for this instance of autocomplete.
* @return {Promise}
*/ */
var activatePreviousSelection = function(state) { var activatePreviousSelection = function(state) {
// Find the list of selections. // Find the list of selections.
...@@ -238,34 +260,40 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -238,34 +260,40 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Find the active one. // Find the active one.
var element = selectionsElement.children('[data-active-selection=true]'); var element = selectionsElement.children('[data-active-selection=true]');
if (!element) { if (!element) {
activateSelection(0, state); return activateSelection(0, state);
return;
} }
// Find it's index. // Find it's index.
var current = selectionsElement.children('[aria-selected=true]').index(element); var current = selectionsElement.children('[aria-selected=true]').index(element);
// Activate the next one. // Activate the next one.
activateSelection(current - 1, state); return activateSelection(current - 1, state);
}; };
/** /**
* Find the index of the current active selection, and activate the next one. * Find the index of the current active selection, and activate the next one.
* *
* @method activateNextSelection * @method activateNextSelection
* @private * @private
* @param {Object} state State variables for this instance of autocomplete. * @param {Object} state State variables for this instance of autocomplete.
* @return {Promise}
*/ */
var activateNextSelection = function(state) { var activateNextSelection = function(state) {
// Find the list of selections. // Find the list of selections.
var selectionsElement = $(document.getElementById(state.selectionId)); var selectionsElement = $(document.getElementById(state.selectionId));
// Find the active one. // Find the active one.
var element = selectionsElement.children('[data-active-selection=true]'); var element = selectionsElement.children('[data-active-selection=true]');
if (!element) { var current = 0;
activateSelection(0, state);
return; if (element) {
// The element was found. Determine the index and move to the next one.
current = selectionsElement.children('[aria-selected=true]').index(element);
current = current + 1;
} else {
// No selected item found. Move to the first.
current = 0;
} }
// Find it's index.
var current = selectionsElement.children('[aria-selected=true]').index(element); return activateSelection(current, state);
// Activate the next one.
activateSelection(current + 1, state);
}; };
/** /**
...@@ -274,16 +302,20 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -274,16 +302,20 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method activatePreviousItem * @method activatePreviousItem
* @private * @private
* @param {Object} state State variables for this autocomplete element. * @param {Object} state State variables for this autocomplete element.
* @return {Promise}
*/ */
var activatePreviousItem = function(state) { var activatePreviousItem = function(state) {
// Find the list of suggestions. // Find the list of suggestions.
var suggestionsElement = $(document.getElementById(state.suggestionsId)); var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Find the active one. // Find the active one.
var element = suggestionsElement.children('[aria-selected=true]'); var element = suggestionsElement.children('[aria-selected=true]');
// Find it's index. // Find it's index.
var current = suggestionsElement.children('[aria-hidden=false]').index(element); var current = suggestionsElement.children('[aria-hidden=false]').index(element);
// Activate the next one.
activateItem(current - 1, state); // Activate the previous one.
return activateItem(current - 1, state);
}; };
/** /**
...@@ -292,6 +324,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -292,6 +324,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method closeSuggestions * @method closeSuggestions
* @private * @private
* @param {Object} state State variables for this autocomplete element. * @param {Object} state State variables for this autocomplete element.
* @return {Promise}
*/ */
var closeSuggestions = function(state) { var closeSuggestions = function(state) {
// Find the elements in the DOM. // Find the elements in the DOM.
...@@ -300,8 +333,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -300,8 +333,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Announce the list of suggestions was closed, and read the current list of selections. // Announce the list of suggestions was closed, and read the current list of selections.
inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId); inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId);
// Hide the suggestions list (from screen readers too). // Hide the suggestions list (from screen readers too).
suggestionsElement.hide().attr('aria-hidden', true); suggestionsElement.hide().attr('aria-hidden', true);
return $.Deferred().resolve();
}; };
/** /**
...@@ -313,8 +349,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -313,8 +349,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} state The state variables for this autocomplete. * @param {Object} state The state variables for this autocomplete.
* @param {String} query The current text for the search string. * @param {String} query The current text for the search string.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list. * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @return {Promise}
*/ */
var updateSuggestions = function(options, state, query, originalSelect) { var updateSuggestions = function(options, state, query, originalSelect) {
var pendingKey = 'form-autocomplete-updateSuggestions-' + state.inputId;
M.util.js_pending(pendingKey);
// Find the elements in the DOM. // Find the elements in the DOM.
var inputElement = $(document.getElementById(state.inputId)); var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId)); var suggestionsElement = $(document.getElementById(state.suggestionsId));
...@@ -332,12 +372,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -332,12 +372,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Re-render the list of suggestions. // Re-render the list of suggestions.
var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase(); var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
var context = $.extend({options: suggestions}, options, state); var context = $.extend({options: suggestions}, options, state);
templates.render( var returnVal = templates.render(
'core/form_autocomplete_suggestions', 'core/form_autocomplete_suggestions',
context context
).done(function(newHTML) { )
.then(function(html, js) {
// We have the new template, insert it in the page. // We have the new template, insert it in the page.
suggestionsElement.replaceWith(newHTML); templates.replaceNode(suggestionsElement, html, js);
// Get the element again. // Get the element again.
suggestionsElement = $(document.getElementById(state.suggestionsId)); suggestionsElement = $(document.getElementById(state.suggestionsId));
// Show it if it is hidden. // Show it if it is hidden.
...@@ -371,8 +413,15 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -371,8 +413,15 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
suggestionsElement.html(nosuggestionsstr); suggestionsElement.html(nosuggestionsstr);
}); });
} }
}).fail(notification.exception);
return suggestionsElement;
})
.then(function() {
return M.util.js_complete(pendingKey);
})
.catch(notification.exception);
return returnVal;
}; };
/** /**
...@@ -383,6 +432,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -383,6 +432,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} options The original options for the autocomplete. * @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete. * @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list. * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @return {Promise}
*/ */
var createItem = function(options, state, originalSelect) { var createItem = function(options, state, originalSelect) {
// Find the element in the DOM. // Find the element in the DOM.
...@@ -419,13 +469,23 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -419,13 +469,23 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
} }
}); });
updateSelectionList(options, state, originalSelect); return updateSelectionList(options, state, originalSelect)
// Notifiy that the selection changed. .then(function() {
// Notify that the selection changed.
notifyChange(originalSelect); notifyChange(originalSelect);
return;
})
.then(function() {
// Clear the input field. // Clear the input field.
inputElement.val(''); inputElement.val('');
return;
})
.then(function() {
// Close the suggestions list. // Close the suggestions list.
closeSuggestions(state); return closeSuggestions(state);
});
}; };
/** /**
...@@ -436,6 +496,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -436,6 +496,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} options The original options for the autocomplete. * @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete. * @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list. * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @return {Promise}
*/ */
var selectCurrentItem = function(options, state, originalSelect) { var selectCurrentItem = function(options, state, originalSelect) {
// Find the elements in the page. // Find the elements in the page.
...@@ -458,22 +519,26 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -458,22 +519,26 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
} }
}); });
// Rerender the selection list. return updateSelectionList(options, state, originalSelect)
updateSelectionList(options, state, originalSelect); .then(function() {
// Notifiy that the selection changed. // Notify that the selection changed.
notifyChange(originalSelect); notifyChange(originalSelect);
return;
})
.then(function() {
if (options.closeSuggestionsOnSelect) { if (options.closeSuggestionsOnSelect) {
// Clear the input element. // Clear the input element.
inputElement.val(''); inputElement.val('');
// Close the list of suggestions. // Close the list of suggestions.
closeSuggestions(state); return closeSuggestions(state);
} else { } else {
// Focus on the input element so the suggestions does not auto-close. // Focus on the input element so the suggestions does not auto-close.
inputElement.focus(); inputElement.focus();
// Remove the last selected item from the suggestions list. // Remove the last selected item from the suggestions list.
updateSuggestions(options, state, inputElement.val(), originalSelect); return updateSuggestions(options, state, inputElement.val(), originalSelect);
} }
});
}; };
/** /**
...@@ -486,10 +551,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -486,10 +551,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} state The state variables for the autocomplete. * @param {Object} state The state variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list. * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results. * @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
* @return {Promise}
*/ */
var updateAjax = function(e, options, state, originalSelect, ajaxHandler) { var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
var pendingKey = 'form-autocomplete-updateajax'; var pendingPromise = addPendingJSPromise('updateAjax');
M.util.js_pending(pendingKey);
// Get the query to pass to the ajax function. // Get the query to pass to the ajax function.
var query = $(e.currentTarget).val(); var query = $(e.currentTarget).val();
// Call the transport function to do the ajax (name taken from Select2). // Call the transport function to do the ajax (name taken from Select2).
...@@ -531,12 +597,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -531,12 +597,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
originalSelect.attr('data-notice', processedResults); originalSelect.attr('data-notice', processedResults);
} }
// Update the list of suggestions now from the new values in the select list. // Update the list of suggestions now from the new values in the select list.
updateSuggestions(options, state, '', originalSelect); pendingPromise.resolve(updateSuggestions(options, state, '', originalSelect));
M.util.js_complete(pendingKey);
}, function(error) { }, function(error) {
M.util.js_complete(pendingKey); pendingPromise.reject(error);
notification.exception(error);
}); });
return pendingPromise;
}; };
/** /**
...@@ -553,132 +619,123 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'] ...@@ -553,132 +619,123 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
var inputElement = $(document.getElementById(state.inputId)); var inputElement = $(document.getElementById(state.inputId));
// Add keyboard nav with keydown. // Add keyboard nav with keydown.
inputElement.on('keydown', function(e) { inputElement.on('keydown', function(e) {
var pendingKey = 'form-autocomplete-addnav-' + state.inputId + '-' + e.keyCode; var pendingJsPromise = addPendingJSPromise('addNavigation-' + state.inputId + '-' + e.keyCode);
M.util.js_pending(pendingKey);
switch (e.keyCode) { switch (e.keyCode) {
case KEYS.DOWN: case KEYS.DOWN:
// If the suggestion list is open, move to the next item. // If the suggestion list is open, move to the next item.
if (!options.showSuggestions) { if (!options.showSuggestions) {
// Do not consume this event. // Do not consume this event.
M.util.js_complete(pendingKey); pendingJsPromise.resolve();
return true; return true;
} else if (inputElement.attr('aria-expanded') === "true") { } else if (inputElement.attr('aria-expanded') === "true") {
activateNextItem(state); pendingJsPromise.resolve(activateNextItem(state));
} else { } else {
// Handle ajax population of suggestions. // Handle ajax population of suggestions.
if (!inputElement.val() && options.ajax) { if (!inputElement.val() && options.ajax) {
require([options.ajax], function(ajaxHandler) { require([options.ajax], function(ajaxHandler) {
updateAjax(e, options, state, originalSelect, ajaxHandler); pendingJsPromise.resolve(updateAjax(e, options, state, originalSelect, ajaxHandler));
}); });
} else { } else {
// Open the suggestions list. // Open the suggestions list.
updateSuggestions(options, state, inputElement.val(), originalSelect); pendingJsPromise.resolve(updateSuggestions(options, state, inputElement.val(), originalSelect));
} }
} }
// We handled this event, so prevent it. // We handled this event, so prevent it.
e.preventDefault(); e.preventDefault();
M.util.js_complete(pendingKey);
return false; return false;
case KEYS.UP: case KEYS.UP:
// Choose the previous active item. // Choose the previous active item.
activatePreviousItem(state); pendingJsPromise.resolve(activatePreviousItem(state));
// We handled this event, so prevent it. // We handled this event, so prevent it.
e.preventDefault(); e.preventDefault();
M.util.js_complete(pendingKey);
return false; return false;
case KEYS.ENTER: case KEYS.ENTER:
var suggestionsElement = $(document.getElementById(state.suggestionsId)); var suggestionsElement = $(document.getElementById(state.suggestionsId));
if ((inputElement.attr('aria-expanded') === "true") && if ((inputElement.attr('aria-expanded') === "true") &&
(suggestionsElement.children('[aria-selected=true]').length > 0)) { (suggestionsElement.children('[aria-selected=true]').length > 0)) {
// If the suggestion list has an active item, select it. // If the suggestion list has an active item, select it.
selectCurrentItem(options, state, originalSelect); pendingJsPromise.resolve(selectCurrentItem(options, state, originalSelect));
} else if (options.tags) { } else if (options.tags) {
// If tags are enabled, create a tag. // If tags are enabled, create a tag.
createItem(options, state, originalSelect); pendingJsPromise.resolve(createItem(options, state, originalSelect));
} else {
pendingJsPromise.resolve();