{"version":3,"file":"Select.js","names":["Select","a","setters","ajax","mediaQuery","AbstractField","default","on","off","trigger","Event","deepMerge","appendUrl","mix","LoaderMixin","Accessibility","tryParseJSON","execute","with","constructor","element","options","arguments","length","native","container","closeOnSelect","redirectUrl","redirectNewTab","position","autoPositionPanel","useIcon","filter","closeTimeout","scrollIntoView","block","a11y","filterLabel","filterButtonLabel","filterResult","filterResults","filterNoResults","classNames","opened","disabled","active","hidden","focus","hover","selected","srOnly","panel","list","listItem","placeholder","hasDescription","text","description","badge","badgeImage","textWithDescription","iconState","icon","optionsMap","optionsMapUrl","selectFirstOptionOnReset","_resizable","trackFocusedElements","initCache","wrapField","classes","filterPanel","filterInput","filterButton","selectors","document","querySelector","checkNative","state","initCustomSelectCache","currentPanelItem","selectOptions","field","querySelectorAll","createCustomPanel","createPlaceholder","initState","defaultOption","index","selectedIndex","value","selectedOption","autoPosition","isOpened","isPreventChange","hasInit","filteredItems","fieldState","loadOptionsMap","masterValue","disable","addLoader","url","master","then","onOptionsMapLoaded","bind","catch","onOptionsMapFailed","data","newOptions","updateOptions","enable","removeLoader","setValueAfterLoad","getAttribute","setValue","console","log","wrapper","createElement","selectElement","hadCurrentFocus","activeElement","classList","add","parentElement","insertBefore","appendChild","fieldWrapper","bindEvents","id","onChange","onNativeSelectItemKeyDown","bindCustomSelectEvents","customOptionsUpdate","panelButton","forEach","item","includes","dataset","remove","selectItem","setSelectedIndex","triggerStateEvents","event","newState","action","detail","dependency","stateDependencies","bubbles","dependentActions","masterField","redirect","openNewTab","redirectType","window","open","e","stopPropagation","currentTarget","saveLastFocusState","selectedItem","clearFilter","setAttribute","selectedPanelItem","currentItemIndex","isDisabledItem","disableItem","enableItem","analytics","placement","innerTextToLowerCase","selectedOptions","innerText","toLowerCase","eventData","category","replace","label","labelHeading","ecommerce","eventType","extraData","Object","keys","key","emit","selectPanelItem","innerHTML","setPlaceholderText","buttonText","span","updateValue","Array","isArray","map","val","opt","option","onPanelClick","onSelectItemKeyDown","onKeyTogglePanel","onFilterChange","onClearFilter","preventDefault","togglePanel","contains","target","closePanel","onMouseEnter","onMouseLeave","onSelectHover","onSelectLeave","error","unbindCustomSelectEvents","destroy","afterInit","restoreLastFocusState","isOptionsWithDescription","some","jsDescription","initFieldState","isNative","is","destroyCustomSelect","ul","fragment","createDocumentFragment","customOption","createCustomOption","jsDisabled","filterForm","createFilterForm","createFilterNoResult","li","jsBucket","hasAttribute","createIcon","jsBadge","createText","badgeSpan","createBadge","image","textSpan","div","descriptionSpan","createDescription","title","style","backgroundColor","color","textColor","button","labelElement","prepend","openPanel","clientWidth","width","display","openPanelEvent","SystemJS","import","createPopper","modifiers","name","padding","onFirstUpdate","selectedElement","leaveTimeout","clearTimeout","setTimeout","unSelectPanelItem","HTMLElement","setValueByAttribute","attribute","attributeValue","skipValidation","push","highLightSearchText","isTextMatching","textContent","isBucketMatching","removeAttribute","setCurrentPanelItem","toggleNoResult","term","indexOf","filterValue","getHighlightTerm","fullText","result","slice","onMediaQueryChange","onResize","reset","removeValidationClass","removeCustomSelect","removeChild","shiftKey","onKeyClosePanel","stopImmediatePropagation","focusPanelItem","onKeyOpenPanel","isActionKey","altKey","setFieldState","filteredItemsCount"],"sources":["components/global/Select.js"],"sourcesContent":["/* eslint max-lines: off */\n/* eslint complexity: off */\n/**\n * Created by xiu on 02/11/2017.\n */\nimport { ajax } from 'toolbox/ajax';\nimport { mediaQuery } from 'toolbox/mediaQuery';\nimport AbstractField from 'components/global/AbstractField';\nimport { on, off, trigger } from 'toolbox/event';\nimport { Event } from 'services/EventEmitter';\nimport { deepMerge } from 'toolbox/deepMerge';\nimport { appendUrl } from 'toolbox/queryString';\nimport { mix } from 'core/mixwith';\nimport LoaderMixin from 'mixins/Loader';\nimport Accessibility from 'mixins/Accessibility';\nimport { tryParseJSON } from 'toolbox/object';\n\n/**\n * This is a description of the Select constructor function.\n * @see styleguide {@link https://ecom-frontend-styleguide.e-loreal.com/mf-lora/category/forms/select}\n * @see confluence {@link https://confluence.e-loreal.com/display/EP/NG+LORA+-+Select}\n * @class\n * @classdesc This is a description of the Select class.\n * @extends Component\n */\nexport default class Select extends mix(AbstractField).with(Accessibility, LoaderMixin) {\n /**\n * Constructor of the class that mainly merge the options of the components\n * @param {HTMLElement} element HTMLElement of the component\n * @param {Object} options options that belongs to the component\n */\n constructor(element, options = {}) {\n super(element, deepMerge({\n native: true, // Display of the native dropdown\n container: false, // Append custom dropdown to a specific element. You need to define this option as css selector.\n closeOnSelect: true, // work only if native is false. When a value is selected, it closes the panel\n redirectUrl: false, // In case the change of the select trigger a redirection - value of option should be a link value\n redirectNewTab: false, // Determines if redirection should open a new tab or use the current one\n trigger: 'click', // can be either click or hover\n position: 'bottom-start', // https://popper.js.org/docs/v2/\n autoPositionPanel: true, // works only if native is false.Allow to update the position of the panel if out of viewport\n useIcon: false, // Works only if native is false. Allows having icon in the select\n filter: false, // Works only if native is false. Allows having filter input in the select\n closeTimeout: 200, // Works only if trigger is hover and native is false. Set a delay before the panel is closed\n scrollIntoView: { // Works only for accessibility when keys UP/DOWN are used.\n block: 'nearest',\n },\n a11y: {\n filterLabel: '', // Works only if native is false. Allows having filter label in the select\n filterButtonLabel: '', // Works only if native is false. Allows having filter label in the select\n filterResult: '', // Works only if native is false. Allows having filter text if there is only 1 result in the selection\n filterResults: '', // Works only if native is false. Allows having filter text if there are more than 1 results in the selection\n filterNoResults: '', // Works only if native is false. Allows having filter text if no result in the select\n },\n classNames: {\n container: null,\n opened: 'm-opened',\n disabled: 'm-disabled',\n active: 'm-active',\n hidden: 'h-hidden',\n focus: 'm-focus',\n hover: 'm-hover',\n selected: 'm-selected',\n srOnly: 'h-show-for-sr',\n panel: 'c-select__panel',\n list: 'c-select__panel-list',\n listItem: 'c-select__panel-item',\n placeholder: 'c-select__placeholder',\n hasDescription: 'm-rows-2',\n text: 'c-select__text',\n description: 'c-select__description',\n badge: 'c-select__badge',\n badgeImage: 'c-select__badge-image',\n textWithDescription: 'c-select__description-wrapper',\n iconState: 'm-icon',\n icon: ['c-select__icon'],\n },\n optionsMap: {}, // select options map, used to dynamically replace select options\n optionsMapUrl: null, // options map url, used to asyncronously load the options map\n selectFirstOptionOnReset: true, // select the first option on reset event\n _resizable: true,\n trackFocusedElements: true, // enable tracking of focused elements\n }, options));\n }\n\n /**\n * All selectors must be cached. Never cache elements that are out of the component scope\n */\n initCache() {\n this.wrapField();\n super.initCache();\n\n this.classes.filterPanel = 'c-select__filter';\n this.classes.filterInput = 'c-select__filter-field';\n this.classes.filterButton = 'c-select__filter-button';\n this.classes.filterNoResults = 'c-select__filter-result';\n this.selectors.container = this.options.container ? document.querySelector(this.options.container) : this.element;\n\n this.checkNative();\n\n // if not native, we create a custom panel based on the select options\n if (!this.state.native) {\n this.initCustomSelectCache();\n }\n }\n\n /**\n * Cache custom select elements\n */\n initCustomSelectCache() {\n this.selectors.currentPanelItem = null;\n this.selectors.selectOptions = this.field.querySelectorAll('option');\n this.options.filter = this.selectors.selectOptions.length > 1 ? this.options.filter : false;\n // Create a panel with list of select options\n this.selectors.panel = this.createCustomPanel();\n // Create a placeholder button for having custom select\n this.selectors.placeholder = this.createPlaceholder();\n\n if (this.selectors.panel) {\n this.selectors.list = this.selectors.panel.querySelector(`.${this.options.classNames.list}`);\n this.selectors.listItem = this.selectors.panel.querySelectorAll(`.${this.options.classNames.listItem}`);\n }\n }\n\n /**\n * Init the different state of the component\n * It helps to avoid heavy DOM manipulation\n */\n initState() {\n this.state.defaultOption = {\n index: this.field.selectedIndex,\n value: this.field.value,\n };\n\n this.state.selectedOption = this.state.defaultOption;\n this.state.autoPosition = null;\n this.state.isOpened = false;\n this.state.isPreventChange = false;\n this.state.hasInit = true;\n\n if (!this.state.native) {\n this.state.filteredItems = this.selectors.listItem || [];\n }\n\n this.state.optionsMap = this.options.optionsMap;\n this.state.fieldState = null;\n }\n\n /**\n * Load localized options data\n * @param {String} masterValue - optional master value (used to limit large data set)\n * @returns {Promise} ajax call\n */\n loadOptionsMap(masterValue) {\n this.disable();\n this.addLoader(this.element);\n let url = this.options.optionsMapUrl;\n if (masterValue) {\n url = appendUrl(url, { master: masterValue });\n }\n return ajax(url)\n .then(this.onOptionsMapLoaded.bind(this, masterValue))\n .catch(this.onOptionsMapFailed.bind(this, masterValue));\n }\n\n /**\n * Callback on ajax request for getting options map\n * @param {Object} masterValue - additional callback params\n * @param {Object} data - response data\n */\n onOptionsMapLoaded(masterValue, data) {\n if (masterValue) {\n this.state.optionsMap[masterValue] = data[masterValue];\n } else {\n this.state.optionsMap = data;\n }\n const newOptions = this.state.optionsMap[masterValue];\n if (newOptions) {\n this.updateOptions(newOptions);\n }\n this.enable();\n this.removeLoader(this.element);\n this.setValueAfterLoad();\n }\n\n /**\n * Set value after lazy loading options\n */\n setValueAfterLoad() {\n const value = this.element.getAttribute('data-lazyfield-value');\n if (value) {\n this.setValue(value);\n this.field.focus();\n }\n }\n\n /**\n * Callback on ajax request for getting options map\n * @param {Object} data - response data\n */\n onOptionsMapFailed(data) {\n console.log(`Error during loading select options: ${data}`);\n }\n\n /**\n * Wrap each field into container\n */\n wrapField() {\n const wrapper = document.createElement('div');\n const selectElement = this.element.querySelector('select');\n const hadCurrentFocus = document.activeElement === selectElement;\n wrapper.classList.add('c-select__container');\n if (selectElement) {\n selectElement.parentElement.insertBefore(wrapper, selectElement);\n wrapper.appendChild(selectElement);\n this.selectors.fieldWrapper = wrapper;\n if (hadCurrentFocus) {\n selectElement.focus();\n }\n }\n }\n\n /**\n * Should contain only event listeners and nothing else\n * All the event handlers should be into a separated function. No usage of anonyous function\n */\n bindEvents() {\n super.bindEvents();\n\n on(`change.${this.id}`, this.field, this.onChange.bind(this));\n\n if (!this.state.native) {\n this.bindCustomSelectEvents();\n } else {\n // Accessibility for native events\n on('keydown', this.field, this.onNativeSelectItemKeyDown.bind(this), false);\n }\n }\n\n /**\n * Update options in custom panel. Reset options if no options were passed\n * @param {Array} options new options\n */\n customOptionsUpdate(options) {\n const panelButton = this.selectors.placeholder;\n\n if (!options) {\n this.selectors.listItem.forEach((item) => {\n item.classList.remove(this.options.classNames.hidden);\n });\n this.selectItem(panelButton, this.selectors.listItem[0]);\n this.setSelectedIndex(this.selectors.listItem[0].getAttribute('data-index'));\n return;\n }\n\n this.selectors.listItem.forEach((item) => {\n if (!options.includes(item.dataset.value)) {\n item.classList.add(this.options.classNames.hidden);\n } else {\n item.classList.remove(this.options.classNames.hidden);\n }\n\n if (item.dataset.value === options[0]) {\n this.selectItem(panelButton, item);\n this.setSelectedIndex(item.getAttribute('data-index'));\n }\n });\n }\n\n /**\n * Handle custom state events triggering\n * @param {Event} event optional, available for dependency action handling\n */\n triggerStateEvents(event) {\n // for select field state should be defined by selected value\n // otherwise it would always be \"selected\"\n const newState = this.field.value;\n\n if (this.state.fieldState === null) {\n this.state.fieldState = newState;\n }\n\n const action = event && event.detail && event.detail.action;\n\n if (newState !== this.state.fieldState || action === 'triggerStateEvents') {\n this.state.fieldState = newState;\n ['selected', `selected#${newState}`].forEach((dependency) => {\n if (dependency in this.options.stateDependencies) {\n trigger('field.dependency.state.changed', this.field, { bubbles: true, dependentActions: this.options.stateDependencies[dependency], masterField: this.field });\n }\n });\n }\n }\n\n /**\n * Handler for URL redirection\n *\n * @param {string} redirectUrl - Target URL\n * @param {bool} openNewTab - If true, the URL will be opened in a new tab. Same tab will be used otherwise.\n */\n redirect(redirectUrl, openNewTab) {\n if (redirectUrl) {\n const redirectType = openNewTab ? '_blank' : '_self';\n window.open(redirectUrl, redirectType);\n }\n }\n\n /**\n * Handler for select change event\n *\n * @param {Event} e - event data\n */\n onChange(e) {\n if (this.state.isPreventChange) {\n e.stopPropagation();\n return;\n }\n\n const { selectedIndex, value } = e.currentTarget;\n\n this.state.selectedOption = {\n index: selectedIndex,\n value,\n };\n\n this.saveLastFocusState();\n\n if (!this.state.native) {\n const selectedItem = this.selectors.panel.querySelector(`[data-value=\"${value}\"]`);\n const panelButton = this.selectors.placeholder;\n // update the panel item when a change occur from the select\n if (this.options.filter) {\n this.clearFilter();\n }\n if (selectedItem) {\n this.selectItem(panelButton, selectedItem);\n panelButton.setAttribute('aria-expanded', 'false');\n }\n this.selectors.selectedPanelItem = selectedItem;\n this.currentItemIndex = selectedItem ? Number(selectedItem.getAttribute('data-index')) : 0;\n\n if (this.options.useIcon) {\n // update the panel button availability status when a change occur from the select\n if (this.isDisabledItem(selectedItem)) {\n this.disableItem(panelButton);\n } else {\n this.enableItem(panelButton);\n }\n }\n }\n\n // Redirect the user when a new value is selected\n if (value && value.length > 0 && this.options.redirectUrl && !this.state.isPreventChange) {\n this.redirect(value, this.options.redirectNewTab);\n }\n\n if (this.analytics && this.analytics.placement) {\n const innerTextToLowerCase = e.currentTarget.selectedOptions[0].innerText.toLowerCase();\n const eventData = {\n category: this.analytics.placement,\n action: (this.analytics.action && this.analytics.action.replace('{selectedValue}', innerTextToLowerCase)) || 'select',\n label: this.analytics.label || (this.analytics.labelHeading ? `${this.analytics.labelHeading}::${innerTextToLowerCase}` : innerTextToLowerCase),\n };\n\n if (this.analytics.ecommerce) {\n eventData.ecommerce = this.analytics.ecommerce;\n }\n\n if (this.analytics.eventType) {\n eventData.eventType = this.analytics.eventType;\n }\n\n if (this.analytics.event) {\n eventData.event = this.analytics.event;\n }\n\n if (this.analytics.extraData) {\n eventData.extraData = {};\n\n Object.keys(this.analytics.extraData).forEach((key) => {\n eventData.extraData[key] = this.analytics.extraData[key].replace('{selectedValue}', innerTextToLowerCase);\n });\n }\n\n Event.emit('analytics.event', eventData);\n }\n }\n\n /**\n * Select item on custom select panel\n * @param {HTMLElement} panelButton panel buttom\n * @param {HTMLElement} selectedItem selected item\n */\n selectItem(panelButton, selectedItem) {\n this.selectPanelItem(selectedItem);\n\n selectedItem.setAttribute('aria-selected', true);\n this.selectors.selectedPanelItem.setAttribute('aria-selected', false);\n panelButton.innerHTML = selectedItem.innerHTML;\n\n this.setPlaceholderText(selectedItem, panelButton);\n }\n\n /**\n * Set text to placeholder button\n *\n * @param {HTMLElement} element - panel selected element\n * @param {HTMLElement} placeholder - placeholder button element\n */\n setPlaceholderText(element, placeholder) {\n const buttonText = placeholder.querySelector(`.${this.options.classNames.text}`);\n\n if (element.getAttribute('aria-label') && buttonText) {\n if (buttonText.innerHTML === '') {\n buttonText.classList.add(this.options.classNames.srOnly);\n buttonText.innerHTML = element.getAttribute('aria-label');\n } else {\n const span = document.createElement('span');\n\n buttonText.setAttribute('aria-hidden', true);\n span.classList.add(this.options.classNames.srOnly);\n span.innerHTML = element.getAttribute('aria-label');\n placeholder.appendChild(span);\n }\n }\n }\n\n /**\n * Update options based on master field selected value.\n *\n * @param {Event} e - event data\n */\n updateValue(e) {\n const masterValue = e.detail && e.detail.masterField && e.detail.masterField.value;\n if (this.options.optionsMapUrl && masterValue && !this.state.optionsMap[masterValue]) {\n this.loadOptionsMap(masterValue);\n } else {\n const newOptions = this.state.optionsMap[masterValue];\n if (newOptions) {\n this.updateOptions(newOptions);\n }\n }\n }\n\n /**\n * Update select options.\n *\n * @param {Array} options New options array. Each option value can be either object or string directly.\n */\n updateOptions(options = []) {\n this.field.innerHTML = '';\n\n if (!Array.isArray(options)) {\n options = Object.keys(options).map(key => ({\n val: key,\n label: options[key],\n }));\n }\n\n options.forEach((opt) => {\n const option = document.createElement('option');\n option.text = opt.label;\n option.value = opt.val;\n this.field.add(option);\n\n if (opt.selected) {\n this.field.value = opt.val;\n }\n });\n }\n\n /**\n * Bind events for custom select\n */\n bindCustomSelectEvents() {\n on('click', this.selectors.panel, this.onPanelClick.bind(this));\n\n // Accessibility events\n on('keydown', this.element, this.onSelectItemKeyDown.bind(this), false);\n on('click', this.selectors.placeholder, this.onKeyTogglePanel.bind(this));\n\n if (this.options.filter) {\n on('input', this.selectors.filterInput, this.onFilterChange.bind(this));\n on('click', this.selectors.filterButton, this.onClearFilter.bind(this));\n }\n\n on(`mousedown.${this.id}`, this.field, (e) => {\n // prevent the dropdown to be displayed when custom dropdown is required\n e.preventDefault();\n\n if (this.options.trigger === 'click') {\n this.element.focus();\n this.togglePanel();\n }\n });\n\n if (this.options.trigger === 'click') {\n on(`click.${this.id}`, document, (e) => {\n if (this.state.isOpened && !this.element.contains(e.target)) {\n this.closePanel();\n this.selectors.placeholder.setAttribute('aria-expanded', 'false');\n }\n });\n on(`mouseenter.${this.id}`, this.field, this.onMouseEnter.bind(this));\n on(`mouseleave.${this.id}`, this.field, this.onMouseLeave.bind(this));\n } else if (this.options.trigger === 'hover') {\n on(`mouseenter.${this.id}`, this.element, this.onSelectHover.bind(this));\n on(`mouseleave.${this.id}`, this.element, this.onSelectLeave.bind(this));\n } else {\n console.error('Select component trigger should be either click or hover but got --> ', this.options.trigger);\n }\n }\n\n /**\n * Unbind custom select event\n */\n unbindCustomSelectEvents() {\n off('click', this.selectors.panel);\n // Accessibility events\n off('keydown', this.element);\n off(`mousedown.${this.id}`, this.field);\n\n if (this.options.filter) {\n off('input', this.selectors.filterInput);\n off('click', this.selectors.filterButton);\n }\n\n if (this.options.trigger === 'click') {\n off(`click.${this.id}`, document);\n off(`mouseenter.${this.id}`, this.field);\n off(`mouseleave.${this.id}`, this.field);\n } else if (this.options.trigger === 'hover') {\n off(this.element);\n }\n\n if (this.state.autoPosition) {\n this.state.autoPosition.destroy();\n }\n }\n\n /**\n * After init\n * Run any script after the component is fully initialized\n */\n afterInit() {\n super.afterInit();\n\n this.restoreLastFocusState();\n\n if (this.options.useIcon) {\n this.element.classList.add(this.options.classNames.iconState);\n }\n\n if (!this.state.native) {\n this.selectors.fieldWrapper.classList.add(this.options.classNames.hidden);\n }\n\n const isOptionsWithDescription = this.selectors.selectOptions && [...this.selectors.selectOptions]\n .some(option => option.dataset && option.dataset.jsDescription);\n\n if (isOptionsWithDescription) {\n this.element.classList.add(this.options.classNames.hasDescription);\n }\n\n this.initFieldState();\n }\n\n /**\n * The component can be native depending on the viewport. The state should be defined once and can refresh during resize\n *\n * @returns {Boolean} true or false\n */\n checkNative() {\n const { native } = this.options;\n const isNative = (typeof native === 'boolean' && native)\n || (typeof native === 'string' && mediaQuery.is(native))\n || false;\n\n // If native state changed, we need to bind or unbind the events accordingly\n if (this.state.hasInit && this.state.native !== isNative) {\n if (isNative) {\n // state come from custom to native, destroy the events\n this.destroyCustomSelect();\n } else {\n this.initCustomSelectCache();\n // state come from custom to native, destroy the events\n this.bindCustomSelectEvents();\n }\n }\n\n this.state.native = isNative;\n\n return isNative;\n }\n\n /**\n * Create a panel based on the options\n *\n * @returns {HTMLElement} - create custom panel\n */\n createCustomPanel() {\n const panel = document.createElement('div');\n const ul = document.createElement('ul');\n const fragment = document.createDocumentFragment();\n const { list } = this.options.classNames;\n\n [...this.selectors.selectOptions].forEach((option, index) => {\n const customOption = this.createCustomOption(option, index);\n\n if (option.selected) {\n this.selectors.selectedPanelItem = customOption;\n this.selectPanelItem(customOption);\n customOption.setAttribute('aria-selected', 'true');\n this.currentItemIndex = index;\n }\n\n if (option.dataset.jsDisabled === 'true') {\n this.disableItem(customOption);\n }\n\n fragment.appendChild(customOption);\n });\n\n ul.setAttribute('tabindex', '-1');\n ul.setAttribute('role', 'listbox');\n ul.classList.add(list);\n ul.appendChild(fragment);\n ul.setAttribute('id', `${this.id}-listbox`);\n ul.setAttribute('aria-labelledby', `${this.id}-label`);\n\n panel.classList.add(this.options.classNames.panel);\n\n if (this.options.filter) {\n const filterForm = this.createFilterForm();\n const filterResult = this.createFilterNoResult();\n\n panel.appendChild(filterForm);\n panel.appendChild(filterResult);\n }\n\n // inject collected