UNPKG

@atlassian/aui

Version:

Atlassian User Interface Framework

552 lines (455 loc) 18 kB
'use strict'; import $ from './jquery'; import './button'; import './i18n'; import './spin'; import Option from './internal/select/option'; import amdify from './internal/amdify'; import CustomEvent from './polyfills/custom-event'; import globalize from './internal/globalize'; import keyCode from './key-code'; import ProgressiveDataSet from './progressive-data-set'; import skate from './internal/skate'; import state from './internal/state'; import SuggestionModel from './internal/select/suggestion-model'; import SuggestionsModel from './internal/select/suggestions-model'; import SuggestionsView from './internal/select/suggestions-view'; import template from './internal/select/template'; import uniqueId from './unique-id'; import { INPUT_SUFFIX } from './internal/constants'; var DESELECTED = -1; var NO_HIGHLIGHT = -1; var DEFAULT_SS_PDS_SIZE = 20; function clearElementImage(element) { element._input.removeAttribute('style'); $(element._input).removeClass('aui-select-has-inline-image'); } function deselect(element) { element._select.selectedIndex = DESELECTED; clearElementImage(element); } function hasResults(element) { return element._suggestionModel.getNumberOfResults(); } function waitForAssistive (callback) { setTimeout(callback, 50); } function setBusyState (element) { if (!element._button.isBusy()) { element._button.busy(); element._input.setAttribute('aria-busy', 'true'); element._dropdown.setAttribute('aria-busy', 'true'); } } function setIdleState (element) { element._button.idle(); element._input.setAttribute('aria-busy', 'false'); element._dropdown.setAttribute('aria-busy', 'false'); } function matchPrefix (model, query) { var value = model.get('label').toLowerCase(); return value.indexOf(query.toLowerCase()) === 0; } function hideDropdown (element) { element._suggestionsView.hide(); element._input.setAttribute('aria-expanded', 'false'); } function setInitialVisualState (element) { var initialHighlightedItem = hasResults(element) ? 0 : NO_HIGHLIGHT; element._suggestionModel.highlight(initialHighlightedItem); hideDropdown(element); } function setElementImage (element, imageSource) { $(element._input).addClass('aui-select-has-inline-image'); element._input.setAttribute('style', 'background-image: url(' + imageSource + ')'); } function suggest (element, autoHighlight, query) { element._autoHighlight = autoHighlight; if (query === undefined) { query = element._input.value; } element._progressiveDataSet.query(query); } function setInputImageToHighlightedSuggestion (element) { var imageSource = (element._suggestionModel.highlighted() && element._suggestionModel.highlighted().get('img-src')); if (imageSource) { setElementImage(element, imageSource); } } function setValueAndDisplayFromModel (element, model) { if (!model) { return; } var option = document.createElement('option'); var select = element._select; var value = model.get('value') || model.get('label'); option.setAttribute('selected', ''); option.setAttribute('value', value); option.textContent = model.getLabel(); // Sync element value. element._input.value = option.textContent; select.innerHTML = ''; select.options.add(option); select.dispatchEvent(new CustomEvent('change', {bubbles: true})); } function clearValue (element) { element._input.value = ''; element._select.innerHTML = ''; } function selectHighlightedSuggestion (element) { setValueAndDisplayFromModel(element, element._suggestionModel.highlighted()); setInputImageToHighlightedSuggestion(element); setInitialVisualState(element); } function convertOptionToModel (option) { return new SuggestionModel(option.serialize()); } function convertOptionsToModels (element) { var models = []; for (var i = 0; i < element._datalist.children.length; i++) { var option = element._datalist.children[i]; models.push(convertOptionToModel(option)); } return models; } function clearAndSet (element, data) { element._suggestionModel.set(); element._suggestionModel.set(data.results); } function getActiveId (select) { var active = select._dropdown.querySelector('.aui-select-active'); return active && active.id; } function getIndexInResults (id, results) { var resultsIds = $.map(results, function (result) { return result.id; }); return resultsIds.indexOf(id); } function createNewValueModel (element) { var option = new Option(); option.setAttribute('value', element._input.value); var newValueSuggestionModel = convertOptionToModel(option); newValueSuggestionModel.set('new-value', true); return newValueSuggestionModel; } function initialiseProgressiveDataSet (element) { element._progressiveDataSet = new ProgressiveDataSet(convertOptionsToModels(element), { model: SuggestionModel, matcher: matchPrefix, queryEndpoint: element._queryEndpoint, maxResults: DEFAULT_SS_PDS_SIZE }); element._isSync = element._queryEndpoint ? false : true; // Progressive data set should indicate whether or not it is busy when processing any async requests. // Check if there's any active queries left, if so: set spinner and state to busy, else set to idle and remove // the spinner. element._progressiveDataSet.on('activity', function () { if (element._progressiveDataSet.activeQueryCount && !element._isSync) { setBusyState(element); state(element).set('should-flag-new-suggestions', false); } else { setIdleState(element); state(element).set('should-flag-new-suggestions', true); } }); // Progressive data set doesn't do anything if the query is empty so we // must manually convert all data list options into models. // // Otherwise progressive data set can do everything else for us: // 1. Sync matching // 2. Async fetching and matching element._progressiveDataSet.on('respond', function (data) { var optionToHighlight; // This means that a query was made before the input was cleared and // we should cancel the response. if (data.query && !element._input.value) { return; } if (state(element).get('should-cancel-response')) { if (!element._progressiveDataSet.activeQueryCount) { state(element).set('should-cancel-response', false); } return; } if (!data.query) { data.results = convertOptionsToModels(element); } var isInputExactMatch = getIndexInResults(element._input.value, data.results) !== -1; var isInputEmpty = !element._input.value; if (element.hasAttribute('can-create-values') && !isInputExactMatch && !isInputEmpty) { data.results.push(createNewValueModel(element)); } if (!state(element).get('should-include-selected')) { var indexOfValueInResults = getIndexInResults(element.value, data.results); if (indexOfValueInResults >= 0) { data.results.splice(indexOfValueInResults, 1); } } clearAndSet(element, data); optionToHighlight = element._suggestionModel.highlighted() || data.results[0]; if (element._autoHighlight) { element._suggestionModel.setHighlighted(optionToHighlight); waitForAssistive(function () { element._input.setAttribute('aria-activedescendant', getActiveId(element)); }); } element._input.setAttribute('aria-expanded', 'true'); // If the response is async (append operation), has elements to append and has a highlighted element, we need to update the status. if (!element._isSync && element._suggestionsView.getActive() && state(element).get('should-flag-new-suggestions')) { element.querySelector('.aui-select-status').innerHTML = AJS.I18n.getText('aui.select.new.suggestions'); } element._suggestionsView.show(); if (element._autoHighlight) { waitForAssistive(function () { element._input.setAttribute('aria-activedescendant', getActiveId(element)); }); } }); } function associateDropdownAndTrigger (element) { element._dropdown.id = element._listId; element.querySelector('button').setAttribute('aria-controls', element._listId); } function bindHighlightMouseover (element) { $(element._dropdown).on('mouseover', 'li', function (e) { if (hasResults(element)) { element._suggestionModel.highlight($(e.target).index()); } }); } function bindSelectMousedown (element) { $(element._dropdown).on('mousedown', 'li', function (e) { if (hasResults(element)) { element._suggestionModel.highlight($(e.target).index()); selectHighlightedSuggestion(element); element._suggestionsView.hide(); element._input.removeAttribute('aria-activedescendant'); } else { return false; } }); } function initialiseValue (element) { var option = element._datalist.querySelector('aui-option[selected]'); if (option) { setValueAndDisplayFromModel(element, convertOptionToModel(option)); } } function isQueryInProgress (element) { return element._progressiveDataSet.activeQueryCount > 0; } function focusInHandler (element) { //if there is a selected value the single select should do an empty //search and return everything var searchValue = element.value ? '' : element._input.value; var isInputEmpty = element._input.value === ''; state(element).set('should-include-selected', isInputEmpty); suggest(element, true, searchValue); } function cancelInProgressQueries (element) { if (isQueryInProgress(element)) { state(element).set('should-cancel-response', true); } } function getSelectedLabel(element) { if (element._select.selectedIndex >= 0) { return element._select.options[element._select.selectedIndex].textContent; } } function handleInvalidInputOnFocusOut (element) { var selectCanBeEmpty = !element.hasAttribute('no-empty-values'); var selectionIsEmpty = !element._input.value; var selectionNotExact = element._input.value !== getSelectedLabel(element); var selectionNotValid = selectionIsEmpty || selectionNotExact; if (selectionNotValid) { if (selectCanBeEmpty) { deselect(element); } else { var selection = getSelectedLabel(element); if (typeof selection === 'undefined') { deselect(element) } else { element._input.value = selection; } } } } function handleHighlightOnFocusOut(element) { // Forget the highlighted suggestion. element._suggestionModel.highlight(NO_HIGHLIGHT); } function focusOutHandler (element) { cancelInProgressQueries(element); handleInvalidInputOnFocusOut(element); handleHighlightOnFocusOut(element); hideDropdown(element); } function handleTabOut (element) { var isSuggestionViewVisible = element._suggestionsView.isVisible(); if (isSuggestionViewVisible) { selectHighlightedSuggestion(element); } } let select = skate('aui-select', { template: template, created: function (element) { element._listId = uniqueId(); element._input = element.querySelector('input'); element._select = element.querySelector('select'); element._dropdown = element.querySelector('.aui-popover'); element._datalist = element.querySelector('datalist'); element._button = element.querySelector('button'); element._suggestionsView = new SuggestionsView(element._dropdown, element._input); element._suggestionModel = new SuggestionsModel(); element._suggestionModel.onChange = function (oldSuggestions) { var suggestionsToAdd = []; element._suggestionModel._suggestions.forEach(function (newSuggestion) { var inArray = oldSuggestions.some(function (oldSuggestion) { return newSuggestion.id === oldSuggestion.id; }); if (!inArray) { suggestionsToAdd.push(newSuggestion); } }); element._suggestionsView.render(suggestionsToAdd, oldSuggestions.length, element._listId); }; element._suggestionModel.onHighlightChange = function () { var active = element._suggestionModel.highlightedIndex(); element._suggestionsView.setActive(active); element._input.setAttribute('aria-activedescendant', getActiveId(element)); }; }, attached: function (element) { skate.init(element); initialiseProgressiveDataSet(element); associateDropdownAndTrigger(element); element._input.setAttribute('aria-controls', element._listId); element.setAttribute('tabindex', '-1'); bindHighlightMouseover(element); bindSelectMousedown(element); initialiseValue(element); setInitialVisualState(element); setInputImageToHighlightedSuggestion(element); }, attributes: { id(element, data) { if (element.id) { element.querySelector('input').id = data.newValue + INPUT_SUFFIX; } }, name(element, data) { element.querySelector('select').setAttribute('name', data.newValue); }, placeholder(element, data) { element.querySelector('input').setAttribute('placeholder', data.newValue); }, src(element, data) { element._queryEndpoint = data.newValue; } }, events: { 'blur input': function (element) { focusOutHandler(element); }, 'mousedown button': function (element) { if (document.activeElement === element._input && element._dropdown.getAttribute('aria-hidden') === 'false') { state(element).set('prevent-open-on-button-click', true); } }, 'click input': function (element) { focusInHandler(element); }, 'click button': function (element) { var data = state(element); if (data.get('prevent-open-on-button-click')) { data.set('prevent-open-on-button-click', false); } else { element.focus(); } }, input: function (element) { if (!element._input.value) { hideDropdown(element); } else { state(element).set('should-include-selected', true); suggest(element, true); } }, 'keydown input': function (element, e) { var currentValue = element._input.value; var handled = false; if (e.keyCode === keyCode.ESCAPE) { cancelInProgressQueries(element); hideDropdown(element); return; } var isSuggestionViewVisible = element._suggestionsView.isVisible(); if (isSuggestionViewVisible && hasResults(element)) { if (e.keyCode === keyCode.ENTER) { cancelInProgressQueries(element); selectHighlightedSuggestion(element); e.preventDefault(); } else if (e.keyCode === keyCode.TAB) { handleTabOut(element); handled = true; } else if (e.keyCode === keyCode.UP) { element._suggestionModel.highlightPrevious(); e.preventDefault(); } else if (e.keyCode === keyCode.DOWN) { element._suggestionModel.highlightNext(); e.preventDefault(); } } else if (e.keyCode === keyCode.UP || e.keyCode === keyCode.DOWN) { focusInHandler(element); e.preventDefault(); } handled = handled || e.defaultPrevented; setTimeout(function emulateCrossBrowserInputEvent () { if (element._input.value !== currentValue && !handled) { element.dispatchEvent(new CustomEvent('input', {bubbles: true})); } }, 0); } }, prototype: { get value () { var selected = this._select.options[this._select.selectedIndex]; return selected ? selected.value : ''; }, set value (value) { if (value === '') { clearValue(this); } else if (value) { var data = this._progressiveDataSet; var model = data.findWhere({ value: value }) || data.findWhere({ label: value }); // Create a new value if allowed and the value doesn't exist. if (!model && this.hasAttribute('can-create-values')) { model = new SuggestionModel({value: value, label: value}); } setValueAndDisplayFromModel(this, model); } return this; }, get displayValue () { return this._input.value; }, blur: function () { this._input.blur(); focusOutHandler(this); return this; }, focus: function () { this._input.focus(); focusInHandler(this); return this; } } }); amdify('aui/select', select); globalize('select', select); export default select;