UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

1,117 lines 58.4 kB
import * as tslib_1 from "tslib";
import * as React from 'react';
import { Callout } from '../../Callout';
import { Label } from '../../Label';
import { CommandButton, IconButton } from '../../Button';
import { Autofill } from '../Autofill/Autofill';
import { autobind, BaseComponent, divProperties, findIndex, getId, getNativeProps, customizable } from '../../Utilities';
import { SelectableOptionMenuItemType } from '../../utilities/selectableOption/SelectableOption.types';
import { getStyles, getOptionStyles, getCaretDownButtonStyles } from './ComboBox.styles';
import { getClassNames, getComboBoxOptionClassNames } from './ComboBox.classNames';
var SearchDirection;
(function (SearchDirection) {
    SearchDirection[SearchDirection["backward"] = -1] = "backward";
    SearchDirection[SearchDirection["none"] = 0] = "none";
    SearchDirection[SearchDirection["forward"] = 1] = "forward";
})(SearchDirection || (SearchDirection = {}));
var HoverStatus;
(function (HoverStatus) {
    // This is used when the user was hovering
    // and has since moused out of the menu items
    HoverStatus[HoverStatus["clearAll"] = -2] = "clearAll";
    // This is the default "normal" state
    // when no hover has happened or a hover is in progress
    HoverStatus[HoverStatus["default"] = -1] = "default";
})(HoverStatus || (HoverStatus = {}));
var ComboBox = /** @class */ (function (_super) {
    tslib_1.__extends(ComboBox, _super);
    function ComboBox(props) {
        var _this = _super.call(this, props) || this;
        // This is used to clear any pending autocomplete
        // text (used when autocomplete is true and allowFreeform is false)
        _this._readOnlyPendingAutoCompleteTimeout = 1000 /* ms */;
        _this._scrollIdleDelay = 250 /* ms */;
        _this._onOptionMouseLeave = function () {
            if (!_this._isScrollIdle) {
                return;
            }
            _this.setState({
                currentPendingValueValidIndexOnHover: HoverStatus.clearAll
            });
        };
        _this._warnMutuallyExclusive({
            'defaultSelectedKey': 'selectedKey',
            'value': 'defaultSelectedKey',
            'selectedKey': 'value',
            'dropdownWidth': 'useComboBoxAsMenuWidth'
        });
        _this._id = props.id || getId('ComboBox');
        var selectedKey = props.defaultSelectedKey !== undefined ? props.defaultSelectedKey : props.selectedKey;
        _this._isScrollIdle = true;
        var index = _this._getSelectedIndex(props.options, selectedKey);
        _this.state = {
            isOpen: false,
            selectedIndex: index,
            focused: false,
            suggestedDisplayValue: '',
            currentOptions: _this.props.options,
            currentPendingValueValidIndex: -1,
            currentPendingValue: '',
            currentPendingValueValidIndexOnHover: HoverStatus.default
        };
        return _this;
    }
    ComboBox.prototype.componentDidMount = function () {
        // hook up resolving the options if needed on focus
        this._events.on(this._comboBoxWrapper, 'focus', this._onResolveOptions, true);
    };
    ComboBox.prototype.componentWillReceiveProps = function (newProps) {
        // Update the selectedIndex and currentOptions state if
        // the selectedKey, value, or options have changed
        if (newProps.selectedKey !== this.props.selectedKey ||
            newProps.value !== this.props.value ||
            newProps.options !== this.props.options) {
            var index = this._getSelectedIndex(newProps.options, newProps.selectedKey);
            this.setState({
                selectedIndex: index,
                currentOptions: newProps.options
            });
        }
    };
    ComboBox.prototype.componentDidUpdate = function (prevProps, prevState) {
        var _this = this;
        var _a = this.props, allowFreeform = _a.allowFreeform, value = _a.value, onMenuOpen = _a.onMenuOpen, onMenuDismissed = _a.onMenuDismissed;
        var _b = this.state, isOpen = _b.isOpen, focused = _b.focused, selectedIndex = _b.selectedIndex, currentPendingValueValidIndex = _b.currentPendingValueValidIndex;
        // If we are newly open or are open and the pending valid index changed,
        // make sure the currently selected/pending option is scrolled into view
        if (isOpen &&
            (!prevState.isOpen ||
                prevState.currentPendingValueValidIndex !== currentPendingValueValidIndex)) {
            // Need this timeout so that the selectedElement ref is correctly updated
            this._async.setTimeout(function () { return _this._scrollIntoView(); }, 0);
        }
        // If we are open or we are just closed, shouldFocusAfterClose is set,
        // are focused but we are not the activeElement set focus on the input
        if (isOpen ||
            (prevState.isOpen &&
                !isOpen &&
                this._focusInputAfterClose &&
                focused &&
                document.activeElement !== this._comboBox.inputElement)) {
            this.focus();
        }
        // If we should focusAfterClose AND
        //   just opened/closed the menu OR
        //   are focused AND
        //     updated the selectedIndex with the menu closed OR
        //     are not allowing freeform OR
        //     the value changed
        // we need to set selection
        if (this._focusInputAfterClose && (prevState.isOpen && !isOpen ||
            (focused &&
                ((!isOpen && prevState.selectedIndex !== selectedIndex) ||
                    !allowFreeform || value !== prevProps.value)))) {
            this._select();
        }
        if (isOpen && !prevState.isOpen && onMenuOpen) {
            onMenuOpen();
        }
        if (!isOpen && prevState.isOpen && onMenuDismissed) {
            onMenuDismissed();
        }
    };
    ComboBox.prototype.componentWillUnmount = function () {
        _super.prototype.componentWillUnmount.call(this);
        // remove the eventHanlder that was added in componentDidMount
        this._events.off(this._comboBoxWrapper);
    };
    // Primary Render
    ComboBox.prototype.render = function () {
        var id = this._id;
        var _a = this.props, className = _a.className, label = _a.label, disabled = _a.disabled, ariaLabel = _a.ariaLabel, required = _a.required, errorMessage = _a.errorMessage, _b = _a.onRenderContainer, onRenderContainer = _b === void 0 ? this._onRenderContainer : _b, _c = _a.onRenderList, onRenderList = _c === void 0 ? this._onRenderList : _c, _d = _a.onRenderItem, onRenderItem = _d === void 0 ? this._onRenderItem : _d, _e = _a.onRenderOption, onRenderOption = _e === void 0 ? this._onRenderOption : _e, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete, buttonIconProps = _a.buttonIconProps, _f = _a.isButtonAriaHidden, isButtonAriaHidden = _f === void 0 ? true : _f, customStyles = _a.styles, theme = _a.theme, title = _a.title;
        var _g = this.state, isOpen = _g.isOpen, selectedIndex = _g.selectedIndex, focused = _g.focused, suggestedDisplayValue = _g.suggestedDisplayValue;
        this._currentVisibleValue = this._getVisibleValue();
        var divProps = getNativeProps(this.props, divProperties);
        var hasErrorMessage = (errorMessage && errorMessage.length > 0) ? true : false;
        this._classNames = getClassNames(getStyles(theme, customStyles), className, !!isOpen, !!disabled, !!required, !!focused, !!allowFreeform, !!hasErrorMessage);
        return (React.createElement("div", tslib_1.__assign({}, divProps, { ref: 'root', className: this._classNames.container }),
            label && (React.createElement(Label, { id: id + '-label', disabled: disabled, required: required, htmlFor: id + '-input', className: this._classNames.label }, label)),
            React.createElement("div", { ref: this._resolveRef('_comboBoxWrapper'), id: id + 'wrapper', className: this._classNames.root },
                React.createElement(Autofill, { "data-is-interactable": !disabled, ref: this._resolveRef('_comboBox'), id: id + '-input', className: this._classNames.input, type: 'text', onFocus: this._select, onBlur: this._onBlur, onKeyDown: this._onInputKeyDown, onKeyUp: this._onInputKeyUp, onClick: this._onAutofillClick, onInputValueChange: this._onInputChange, "aria-expanded": isOpen, "aria-autocomplete": this._getAriaAutoCompleteValue(), role: 'combobox', "aria-readonly": ((allowFreeform || disabled) ? null : 'true'), readOnly: disabled || !allowFreeform, "aria-labelledby": (label && (id + '-label')), "aria-label": ((ariaLabel && !label) && ariaLabel), "aria-describedby": (id + '-option'), "aria-activedescendant": this._getAriaActiveDescentValue(), "aria-disabled": disabled, "aria-owns": (id + '-list'), spellCheck: false, defaultVisibleValue: this._currentVisibleValue, suggestedDisplayValue: suggestedDisplayValue, updateValueInWillReceiveProps: this._onUpdateValueInAutofillWillReceiveProps, shouldSelectFullInputValueInComponentDidUpdate: this._onShouldSelectFullInputValueInAutofillComponentDidUpdate, title: title }),
                React.createElement(IconButton, { className: 'ms-ComboBox-CaretDown-button', styles: this._getCaretButtonStyles(), role: 'presentation', "aria-hidden": isButtonAriaHidden, tabIndex: -1, onClick: this._onComboBoxClick, iconProps: buttonIconProps, disabled: disabled, checked: isOpen })),
            isOpen && (onRenderContainer(tslib_1.__assign({}, this.props, { onRenderList: onRenderList,
                onRenderItem: onRenderItem,
                onRenderOption: onRenderOption, options: this.state.currentOptions.map(function (item, index) { return (tslib_1.__assign({}, item, { index: index })); }) }), this._onRenderContainer)),
            errorMessage &&
                React.createElement("div", { className: this._classNames.errorMessage }, errorMessage)));
    };
    /**
     * Set focus on the input
     */
    ComboBox.prototype.focus = function (shouldOpenOnFocus) {
        if (this._comboBox) {
            this._comboBox.focus();
            if (shouldOpenOnFocus) {
                this.setState({
                    isOpen: true
                });
            }
        }
    };
    /**
     * Close menu callout if it is open
     */
    ComboBox.prototype.dismissMenu = function () {
        var isOpen = this.state.isOpen;
        isOpen && this.setState({ isOpen: false });
    };
    /**
     * componentWillReceiveProps handler for the auto fill component
     * Checks/updates the iput value to set, if needed
     * @param {IAutofillProps} defaultVisibleValue - the defaultVisibleValue that got passed
     *  in to the auto fill's componentWillReceiveProps
     * @returns {string} - the updated value to set, if needed
     */
    ComboBox.prototype._onUpdateValueInAutofillWillReceiveProps = function () {
        if (this._comboBox === null || this._comboBox === undefined) {
            return null;
        }
        if (this._currentVisibleValue && this._currentVisibleValue !== '' && this._comboBox.value !== this._currentVisibleValue) {
            return this._currentVisibleValue;
        }
        return this._comboBox.value;
    };
    /**
     * componentDidUpdate handler for the auto fill component
     *
     * @param { string } defaultVisibleValue - the current defaultVisibleValue in the auto fill's componentDidUpdate
     * @param { string } suggestedDisplayValue - the current suggestedDisplayValue in the auto fill's componentDidUpdate
     * @returns {boolean} - should the full value of the input be selected?
     * True if the defaultVisibleValue equals the suggestedDisplayValue, false otherwise
     */
    ComboBox.prototype._onShouldSelectFullInputValueInAutofillComponentDidUpdate = function () {
        return this._currentVisibleValue === this.state.suggestedDisplayValue;
    };
    /**
     * Get the correct value to pass to the input
     * to show to the user based off of the current props and state
     * @returns {string} the value to pass to the input
     */
    ComboBox.prototype._getVisibleValue = function () {
        var _a = this.props, value = _a.value, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
        var _b = this.state, selectedIndex = _b.selectedIndex, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentOptions = _b.currentOptions, currentPendingValue = _b.currentPendingValue, suggestedDisplayValue = _b.suggestedDisplayValue, isOpen = _b.isOpen;
        var currentPendingIndexValid = this._indexWithinBounds(currentOptions, currentPendingValueValidIndex);
        // If the user passed is a value prop, use that
        // unless we are open and have a valid current pending index
        if (!(isOpen && currentPendingIndexValid) && (value && !currentPendingValue)) {
            return value;
        }
        var index = selectedIndex;
        if (allowFreeform) {
            // If we are allowing freeform and autocomplete is also true
            // and we've got a pending value that matches an option, remember
            // the matched option's index
            if (autoComplete === 'on' && currentPendingIndexValid) {
                index = currentPendingValueValidIndex;
            }
            // Since we are allowing freeform, if there is currently a nonempty pending value, use that
            // otherwise use the index determined above (falling back to '' if we did not get a valid index)
            return currentPendingValue !== '' ? currentPendingValue : (this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : '');
        }
        else {
            // If we are not allowing freeform and have a
            // valid index that matches the pending value,
            // we know we will need some version of the pending value
            if (currentPendingIndexValid) {
                // If autoComplete is on, return the
                // raw pending value, otherwise remember
                // the matched option's index
                if (autoComplete === 'on') {
                    return currentPendingValue;
                }
                index = currentPendingValueValidIndex;
            }
            // If we have a valid index then return the text value of that option,
            // otherwise return the suggestedDisplayValue
            return this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue;
        }
    };
    /**
     * Is the index within the bounds of the array?
     * @param options - options to check if the index is valid for
     * @param index - the index to check
     * @returns {boolean} - true if the index is valid for the given options, false otherwise
     */
    ComboBox.prototype._indexWithinBounds = function (options, index) {
        if (!options) {
            return false;
        }
        return index >= 0 && index < options.length;
    };
    /**
     * Handler for typing changes on the input
     * @param updatedValue - the newly changed value
     */
    ComboBox.prototype._onInputChange = function (updatedValue) {
        if (this.props.disabled) {
            this._handleInputWhenDisabled(null /* event */);
            return;
        }
        this.props.allowFreeform ?
            this._processInputChangeWithFreeform(updatedValue) :
            this._processInputChangeWithoutFreeform(updatedValue);
    };
    /**
     * Process the new input's new value when the comboBox
     * allows freeform entry
     * @param updatedValue - the input's newly changed value
     */
    ComboBox.prototype._processInputChangeWithFreeform = function (updatedValue) {
        var currentOptions = this.state.currentOptions;
        // if the new value is empty, nothing needs to be done
        if (updatedValue === '') {
            return;
        }
        // Remember the original value and then,
        // make the value lowercase for comparison
        var originalUpdatedValue = updatedValue;
        updatedValue = updatedValue.toLocaleLowerCase();
        var newSuggestedDisplayValue = '';
        var newCurrentPendingValueValidIndex = -1;
        // If autoComplete is on, attempt to find a match from the available options
        if (this.props.autoComplete === 'on') {
            // If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
            var items = currentOptions.map(function (item, index) { return tslib_1.__assign({}, item, { index: index }); }).filter(function (option) { return option.itemType !== SelectableOptionMenuItemType.Header && option.itemType !== SelectableOptionMenuItemType.Divider; }).filter(function (option) { return option.text.toLocaleLowerCase().indexOf(updatedValue) === 0; });
            if (items.length > 0) {
                // If the user typed out the complete option text, we don't need any suggested display text anymore
                newSuggestedDisplayValue = items[0].text.toLocaleLowerCase() !== updatedValue ? items[0].text : '';
                // remember the index of the match we found
                newCurrentPendingValueValidIndex = items[0].index;
            }
        }
        else {
            // If autoComplete is off, attempt to find a match only when the value is exactly equal to the text of an option
            var items = currentOptions.map(function (item, index) { return tslib_1.__assign({}, item, { index: index }); }).filter(function (option) { return option.itemType !== SelectableOptionMenuItemType.Header && option.itemType !== SelectableOptionMenuItemType.Divider; }).filter(function (option) { return option.text.toLocaleLowerCase() === updatedValue; });
            // if we fould a match remember the index
            if (items.length === 1) {
                newCurrentPendingValueValidIndex = items[0].index;
            }
        }
        // Set the updated state
        this._setPendingInfo(originalUpdatedValue, newCurrentPendingValueValidIndex, newSuggestedDisplayValue);
    };
    /**
     * Process the new input's new value when the comboBox
     * does not allow freeform entry
     * @param updatedValue - the input's newly changed value
     */
    ComboBox.prototype._processInputChangeWithoutFreeform = function (updatedValue) {
        var _this = this;
        var _a = this.state, currentPendingValue = _a.currentPendingValue, currentPendingValueValidIndex = _a.currentPendingValueValidIndex, currentOptions = _a.currentOptions, selectedIndex = _a.selectedIndex;
        if (this.props.autoComplete === 'on') {
            // If autoComplete is on while allow freeform is off,
            // we will remember the keypresses and build up a string to attempt to match
            // as long as characters are typed within a the timeout span of each other,
            // otherwise we will clear the string and start building a new one on the next keypress.
            // Also, only do this processing if we have a non-empty value
            if (updatedValue !== '') {
                // If we have a pending autocomplete clearing task,
                // we know that the user is typing with keypresses happening
                // within the timeout of each other so remove the clearing task
                // and continue building the pending value with the udpated value
                if (this._lastReadOnlyAutoCompleteChangeTimeoutId !== undefined) {
                    this._async.clearTimeout(this._lastReadOnlyAutoCompleteChangeTimeoutId);
                    this._lastReadOnlyAutoCompleteChangeTimeoutId = undefined;
                    updatedValue = currentPendingValue + updatedValue;
                }
                var originalUpdatedValue = updatedValue;
                updatedValue = updatedValue.toLocaleLowerCase();
                // If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
                var items = currentOptions.map(function (item, i) { return tslib_1.__assign({}, item, { index: i }); }).filter(function (option) { return option.itemType !== SelectableOptionMenuItemType.Header && option.itemType !== SelectableOptionMenuItemType.Divider; }).filter(function (option) { return option.text.toLocaleLowerCase().indexOf(updatedValue) === 0; });
                // If we found a match, udpdate the state
                if (items.length > 0) {
                    this._setPendingInfo(originalUpdatedValue, items[0].index, items[0].text);
                }
                // Schedule a timeout to clear the pending value after the timeout span
                this._lastReadOnlyAutoCompleteChangeTimeoutId =
                    this._async.setTimeout(function () { _this._lastReadOnlyAutoCompleteChangeTimeoutId = undefined; }, this._readOnlyPendingAutoCompleteTimeout);
                return;
            }
        }
        // If we get here, either autoComplete is on or we did not find a match with autoComplete on.
        // Remember we are not allowing freeform, so at this point, if we have a pending valid value index
        // use that; otherwise use the selectedIndex
        var index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : selectedIndex;
        // Since we are not allowing freeform, we need to
        // set both the pending and suggested values/index
        // to allow us to select all content in the input to
        // give the illusion that we are readonly (e.g. freeform off)
        this._setPendingInfoFromIndex(index);
    };
    /**
     * Walk along the options starting at the index, stepping by the delta (positive or negative)
     * looking for the next valid selectable index (e.g. skipping headings and dividers)
     * @param index - the index to get the next selectable index from
     * @param delta - optional delta to step by when finding the next index, defaults to 0
     * @returns {number} - the next valid selectable index. If the new index is outside of the bounds,
     * it will snap to the edge of the options array. If delta == 0 and the given index is not selectable
     */
    ComboBox.prototype._getNextSelectableIndex = function (index, searchDirection) {
        var currentOptions = this.state.currentOptions;
        var newIndex = index + searchDirection;
        newIndex = Math.max(0, Math.min(currentOptions.length - 1, newIndex));
        if (!this._indexWithinBounds(currentOptions, newIndex)) {
            return -1;
        }
        var option = currentOptions[newIndex];
        // attempt to skip headers and dividers
        if ((option.itemType === SelectableOptionMenuItemType.Header ||
            option.itemType === SelectableOptionMenuItemType.Divider)) {
            // Should we continue looking for an index to select?
            if (searchDirection !== SearchDirection.none &&
                ((newIndex > 0 && searchDirection < SearchDirection.none) ||
                    (newIndex >= 0 && newIndex < currentOptions.length && searchDirection > SearchDirection.none))) {
                newIndex = this._getNextSelectableIndex(newIndex, searchDirection);
            }
            else {
                // If we cannot perform a useful search just return the index we were given
                return index;
            }
        }
        // We have the next valid selectable index, return it
        return newIndex;
    };
    /**
     * Set the selected index. Note, this is
     * the "real" selected index, not the pending selected index
     * @param index - the index to set (or the index to set from if a search direction is provided)
     * @param searchDirection - the direction to search along the options from the given index
     */
    ComboBox.prototype._setSelectedIndex = function (index, searchDirection) {
        if (searchDirection === void 0) { searchDirection = SearchDirection.none; }
        var onChanged = this.props.onChanged;
        var _a = this.state, selectedIndex = _a.selectedIndex, currentOptions = _a.currentOptions;
        // Find the next selectable index, if searchDirection is none
        // we will get our starting index back
        index = this._getNextSelectableIndex(index, searchDirection);
        if (!this._indexWithinBounds(currentOptions, index)) {
            return;
        }
        // Are we at a new index? If so, update the state, otherwise
        // there is nothing to do
        if (index !== selectedIndex) {
            var option = currentOptions[index];
            // Set the selected option
            this.setState({
                selectedIndex: index
            });
            // Did the creator give us an onChanged callback?
            if (onChanged) {
                onChanged(option, index);
            }
            // if we have a new selected index,
            // clear all of the pending info
            this._clearPendingInfo();
        }
    };
    /**
     * Focus (and select) the content of the input
     * and set the focused state
     */
    ComboBox.prototype._select = function () {
        this._comboBox.inputElement.select();
        if (!this.state.focused) {
            this.setState({ focused: true });
        }
    };
    /**
     * Callback issued when the options should be resolved, if they have been updated or
     * if they need to be passed in the first time. This only does work if an onResolveOptions
     * callback was passed in
     */
    ComboBox.prototype._onResolveOptions = function () {
        var _this = this;
        if (this.props.onResolveOptions) {
            // get the options
            var newOptions = this.props.onResolveOptions(tslib_1.__assign({}, this.state.currentOptions));
            // Check to see if the returned value is an array, if it is update the state
            // If the returned value is not an array then check to see if it's a promise or PromiseLike. If it is then resolve it asynchronously.
            if (Array.isArray(newOptions)) {
                this.setState({
                    currentOptions: newOptions
                });
            }
            else if (newOptions && newOptions.then) {
                // Ensure that the promise will only use the callback if it was the most recent one
                // and update the state when the promise returns
                var promise_1 = this._currentPromise = newOptions;
                promise_1.then(function (newOptionsFromPromise) {
                    if (promise_1 === _this._currentPromise) {
                        _this.setState({
                            currentOptions: newOptionsFromPromise
                        });
                    }
                });
            }
        }
    };
    /**
     * OnBlur handler. Set the focused state to false
     * and submit any pending value
     */
    ComboBox.prototype._onBlur = function (event) {
        // Do nothing if the blur is coming from something
        // inside the comboBox root or the comboBox menu since
        // it we are not really bluring from the whole comboBox
        if (event.relatedTarget &&
            (this.refs.root && this.refs.root.contains(event.relatedTarget) ||
                this._comboBoxMenu && this._comboBoxMenu.contains(event.relatedTarget))) {
            event.preventDefault();
            event.stopPropagation();
            return;
        }
        if (this.state.focused) {
            this.setState({ focused: false });
            this._submitPendingValue();
        }
    };
    /**
     * Submit a pending value if there is one
     */
    ComboBox.prototype._submitPendingValue = function () {
        var _a = this.props, onChanged = _a.onChanged, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
        var _b = this.state, currentPendingValue = _b.currentPendingValue, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentOptions = _b.currentOptions, currentPendingValueValidIndexOnHover = _b.currentPendingValueValidIndexOnHover;
        // If we allow freeform and we have a pending value, we
        // need to handle that
        if (allowFreeform && currentPendingValue !== '') {
            // Check to see if the user typed an exact match
            if (this._indexWithinBounds(currentOptions, currentPendingValueValidIndex)) {
                var pendingOptionText = currentOptions[currentPendingValueValidIndex].text.toLocaleLowerCase();
                // By exact match, that means: our pending value is the same as the the pending option text OR
                // the pending option starts with the pending value and we have an "autoComplete" selection
                // where the total length is equal to pending option length OR
                // the live value in the underlying input matches the pending option; update the state
                if (currentPendingValue.toLocaleLowerCase() === pendingOptionText ||
                    (autoComplete && pendingOptionText.indexOf(currentPendingValue.toLocaleLowerCase()) === 0 &&
                        (this._comboBox.isValueSelected &&
                            currentPendingValue.length + (this._comboBox.selectionEnd - this._comboBox.selectionStart) === pendingOptionText.length) ||
                        (this._comboBox.inputElement.value.toLocaleLowerCase() === pendingOptionText))) {
                    this._setSelectedIndex(currentPendingValueValidIndex);
                    this._clearPendingInfo();
                    return;
                }
            }
            if (onChanged) {
                onChanged(undefined, undefined, currentPendingValue);
            }
            else {
                // If we are not controlled, create a new option
                var newOption = { key: currentPendingValue, text: currentPendingValue };
                var newOptions = currentOptions.concat([newOption]);
                this.setState({
                    currentOptions: newOptions,
                    selectedIndex: newOptions.length - 1
                });
            }
        }
        else if (currentPendingValueValidIndex >= 0) {
            // Since we are not allowing freeform, we must have a matching
            // to be able to update state
            this._setSelectedIndex(currentPendingValueValidIndex);
        }
        else if (currentPendingValueValidIndexOnHover >= 0) {
            // If all else failed and we were hovering over an item, select it
            this._setSelectedIndex(currentPendingValueValidIndexOnHover);
        }
        // Finally, clear the pending info
        this._clearPendingInfo();
    };
    // Render Callout container and pass in list
    ComboBox.prototype._onRenderContainer = function (props) {
        var onRenderList = props.onRenderList, calloutProps = props.calloutProps, dropdownWidth = props.dropdownWidth, _a = props.onRenderLowerContent, onRenderLowerContent = _a === void 0 ? this._onRenderLowerContent : _a, useComboBoxAsMenuWidth = props.useComboBoxAsMenuWidth;
        return (React.createElement(Callout, tslib_1.__assign({ isBeakVisible: false, gapSpace: 0, doNotLayer: false, directionalHint: 4 /* bottomLeftEdge */, directionalHintFixed: true }, calloutProps, { className: this._classNames.callout, target: this._comboBoxWrapper, onDismiss: this._onDismiss, onScroll: this._onScroll, setInitialFocus: false, calloutWidth: useComboBoxAsMenuWidth ?
                this._comboBoxWrapper.clientWidth + 2
                : dropdownWidth }),
            React.createElement("div", { className: this._classNames.optionsContainerWrapper, ref: this._resolveRef('_comboBoxMenu') }, onRenderList(tslib_1.__assign({}, props), this._onRenderList)),
            onRenderLowerContent(this.props, this._onRenderLowerContent)));
    };
    // Render List of items
    ComboBox.prototype._onRenderList = function (props) {
        var _this = this;
        var onRenderItem = props.onRenderItem, options = props.options;
        var id = this._id;
        return (React.createElement("div", { id: id + '-list', className: this._classNames.optionsContainer, "aria-labelledby": id + '-label', role: 'listbox' }, options.map(function (item) { return onRenderItem(item, _this._onRenderItem); })));
    };
    // Render items
    ComboBox.prototype._onRenderItem = function (item) {
        switch (item.itemType) {
            case SelectableOptionMenuItemType.Divider:
                return this._renderSeparator(item);
            case SelectableOptionMenuItemType.Header:
                return this._renderHeader(item);
            default:
                return this._renderOption(item);
        }
    };
    // Default _onRenderLowerContent function returns nothing
    ComboBox.prototype._onRenderLowerContent = function () {
        return null;
    };
    // Render separator
    ComboBox.prototype._renderSeparator = function (item) {
        var index = item.index, key = item.key;
        if (index && index > 0) {
            return (React.createElement("div", { role: 'separator', key: key, className: this._classNames.divider }));
        }
        return null;
    };
    ComboBox.prototype._renderHeader = function (item) {
        var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderOption : _a;
        return (React.createElement("div", { key: item.key, className: this._classNames.header, role: 'heading' }, onRenderOption(item, this._onRenderOption)));
    };
    // Render menu item
    ComboBox.prototype._renderOption = function (item) {
        var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderOption : _a;
        var id = this._id;
        var isSelected = this._isOptionSelected(item.index);
        var rootClassNames = getComboBoxOptionClassNames(this._getCurrentOptionStyles(item)).root;
        return (React.createElement(CommandButton, { id: id + '-list' + item.index, key: item.key, "data-index": item.index, className: rootClassNames, styles: this._getCurrentOptionStyles(item), checked: isSelected, onClick: this._onItemClick(item.index), onMouseEnter: this._onOptionMouseEnter.bind(this, item.index), onMouseMove: this._onOptionMouseMove.bind(this, item.index), onMouseLeave: this._onOptionMouseLeave, role: 'option', "aria-selected": isSelected ? 'true' : 'false', ariaLabel: item.text, disabled: item.disabled },
            " ",
            React.createElement("span", { ref: this._resolveRef(isSelected ? '_selectedElement' : '') }, onRenderOption(item, this._onRenderOption))));
    };
    /**
     * If we are coming from a mouseOut:
     * there is no visible selected option.
     *
     * Else if We are hovering over an item:
     * that gets the selected look.
     *
     * Else:
     * Use the current valid pending index if it exists OR
     * we do not have a valid index and we currently have a pending input value,
     * otherwise use the selected index
     * */
    ComboBox.prototype._isOptionSelected = function (index) {
        var currentPendingValueValidIndexOnHover = this.state.currentPendingValueValidIndexOnHover;
        // If the hover state is set to clearAll, don't show a selected index.
        // Note, this happens when the user moused out of the menu items
        if (currentPendingValueValidIndexOnHover === HoverStatus.clearAll) {
            return false;
        }
        return this._getPendingSelectedIndex(true /* includePendingValue */) === index;
    };
    /**
     * Gets the pending selected index taking into account hover, valueValidIndex, and selectedIndex
     * @param includeCurrentPendingValue - Should we include the currentPendingValue when
     * finding the index
     */
    ComboBox.prototype._getPendingSelectedIndex = function (includeCurrentPendingValue) {
        var _a = this.state, currentPendingValueValidIndexOnHover = _a.currentPendingValueValidIndexOnHover, currentPendingValueValidIndex = _a.currentPendingValueValidIndex, currentPendingValue = _a.currentPendingValue, selectedIndex = _a.selectedIndex;
        return (currentPendingValueValidIndexOnHover >= 0 ?
            currentPendingValueValidIndexOnHover :
            (currentPendingValueValidIndex >= 0 || (includeCurrentPendingValue && currentPendingValue !== '')) ?
                currentPendingValueValidIndex :
                selectedIndex);
    };
    /**
     * Scroll handler for the callout to make sure the mouse events
     * for updating focus are not interacting during scroll
     */
    ComboBox.prototype._onScroll = function () {
        var _this = this;
        if (!this._isScrollIdle && this._scrollIdleTimeoutId !== undefined) {
            this._async.clearTimeout(this._scrollIdleTimeoutId);
            this._scrollIdleTimeoutId = undefined;
        }
        else {
            this._isScrollIdle = false;
        }
        this._scrollIdleTimeoutId = this._async.setTimeout(function () { _this._isScrollIdle = true; }, this._scrollIdleDelay);
    };
    /**
     * Scroll the selected element into view
     */
    ComboBox.prototype._scrollIntoView = function () {
        var _a = this.props, onScrollToItem = _a.onScrollToItem, scrollSelectedToTop = _a.scrollSelectedToTop;
        var _b = this.state, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentPendingValue = _b.currentPendingValue, selectedIndex = _b.selectedIndex;
        if (onScrollToItem) {
            // Use the custom scroll handler
            onScrollToItem((currentPendingValueValidIndex >= 0 || currentPendingValue !== '') ? currentPendingValueValidIndex : selectedIndex);
        }
        else if (this._selectedElement && this._selectedElement.offsetParent) {
            // We are using refs, scroll the ref into view
            if (scrollSelectedToTop) {
                this._selectedElement.offsetParent.scrollIntoView(true);
            }
            else {
                var alignToTop = true;
                if (this._comboBoxMenu.offsetParent) {
                    var scrollableParentRect = this._comboBoxMenu.offsetParent.getBoundingClientRect();
                    var selectedElementRect = this._selectedElement.offsetParent.getBoundingClientRect();
                    // If we are completely in view then we do not need to scroll
                    if (scrollableParentRect.top <= selectedElementRect.top &&
                        scrollableParentRect.top + scrollableParentRect.height >= selectedElementRect.top + selectedElementRect.height) {
                        return;
                    }
                    // If we are lower than the scrollable parent viewport then we should align to the bottom
                    if (scrollableParentRect.top + scrollableParentRect.height <= selectedElementRect.top + selectedElementRect.height) {
                        alignToTop = false;
                    }
                }
                this._selectedElement.offsetParent.scrollIntoView(alignToTop);
            }
        }
    };
    // Render content of item
    ComboBox.prototype._onRenderOption = function (item) {
        var optionClassNames = getComboBoxOptionClassNames(this._getCurrentOptionStyles(item));
        return React.createElement("span", { className: optionClassNames.optionText }, item.text);
    };
    /**
     * Click handler for the menu items
     * to select the item and also close the menu
     * @param index - the index of the item that was clicked
     */
    ComboBox.prototype._onItemClick = function (index) {
        var _this = this;
        return function () {
            _this._setSelectedIndex(index);
            _this.setState({
                isOpen: false
            });
        };
    };
    /**
     * Handles dismissing (cancelling) the menu
     */
    ComboBox.prototype._onDismiss = function () {
        // reset the selected index
        // to the last valud state
        this._resetSelectedIndex();
        // close the menu and focus the input
        this.setState({ isOpen: false });
        if (this._focusInputAfterClose) {
            this._comboBox.focus();
        }
    };
    /**
     * Get the index of the option that is marked as selected
     * @param options - the comboBox options
     * @param selectedKey - the known selected key to find
     * @returns { number } - the index of the selected option, -1 if not found
     */
    ComboBox.prototype._getSelectedIndex = function (options, selectedKey) {
        if (options === undefined || selectedKey === undefined) {
            return -1;
        }
        return findIndex(options, (function (option) { return (option.selected || option.key === selectedKey); }));
    };
    /**
     * Reset the selected index by clearing the
     * input (of any pending text), clearing the pending state,
     * and setting the suggested display value to the last
     * selected state text
     */
    ComboBox.prototype._resetSelectedIndex = function () {
        var _a = this.state, selectedIndex = _a.selectedIndex, currentOptions = _a.currentOptions;
        this._comboBox.clear();
        this._clearPendingInfo();
        if (selectedIndex > 0 && selectedIndex < currentOptions.length) {
            this.setState({
                suggestedDisplayValue: currentOptions[selectedIndex].text
            });
        }
        else if (this.props.value) {
            // If we had a value initially, restore it
            this.setState({
                suggestedDisplayValue: this.props.value
            });
        }
    };
    /**
     * Clears the pending info state
     */
    ComboBox.prototype._clearPendingInfo = function () {
        this._setPendingInfo('' /* suggestedDisplayValue */, -1 /* currentPendingValueValidIndex */, '' /* currentPendingValue */);
    };
    /**
     * Set the pending info
     * @param currentPendingValue - new pending value to set
     * @param currentPendingValueValidIndex - new pending value index to set
     * @param suggestedDisplayValue - new suggest display value to set
     */
    ComboBox.prototype._setPendingInfo = function (currentPendingValue, currentPendingValueValidIndex, suggestedDisplayValue) {
        this.setState({
            currentPendingValue: currentPendingValue,
            currentPendingValueValidIndex: currentPendingValueValidIndex,
            suggestedDisplayValue: suggestedDisplayValue,
            currentPendingValueValidIndexOnHover: HoverStatus.default
        });
    };
    /**
     * Set the pending info from the given index
     * @param index - the index to set the pending info from
     */
    ComboBox.prototype._setPendingInfoFromIndex = function (index) {
        var currentOptions = this.state.currentOptions;
        if (index >= 0 && index < currentOptions.length) {
            var option = currentOptions[index];
            this._setPendingInfo(option.text, index, option.text);
        }
        else {
            this._clearPendingInfo();
        }
    };
    /**
     * Sets the pending info for the comboBox
     * @param index - the index to search from
     * @param searchDirection - the direction to search
     */
    ComboBox.prototype._setPendingInfoFromIndexAndDirection = function (index, searchDirection) {
        var _a = this.state, isOpen = _a.isOpen, selectedIndex = _a.selectedIndex, currentOptions = _a.currentOptions;
        index = this._getNextSelectableIndex(index, searchDirection);
        if (this._indexWithinBounds(currentOptions, index)) {
            this._setPendingInfoFromIndex(index);
        }
    };
    /**
     * Sets the isOpen state and updates focusInputAfterClose
     */
    ComboBox.prototype._setOpenStateAndFocusOnClose = function (isOpen, focusInputAfterClose) {
        this._focusInputAfterClose = focusInputAfterClose;
        this.setState({
            isOpen: isOpen
        });
    };
    /**
     * Handle keydown on the input
     * @param ev - The keyboard event that was fired
     */
    ComboBox.prototype._onInputKeyDown = function (ev) {
        var _a = this.props, disabled = _a.disabled, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
        var _b = this.state, isOpen = _b.isOpen, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentOptions = _b.currentOptions, currentPendingValueValidIndexOnHover = _b.currentPendingValueValidIndexOnHover;
        if (disabled) {
            this._handleInputWhenDisabled(ev);
            return;
        }
        var index = this._getPendingSelectedIndex(false /* includeCurrentPendingValue */);
        switch (ev.which) {
            case 13 /* enter */:
                // On enter submit the pending value
                this._submitPendingValue();
                // if we are open or
                // if we are not allowing freeform or
                // our we have no pending value
                // and no valid pending index
                // flip the open state
                if ((isOpen ||
                    ((!allowFreeform ||
                        this.state.currentPendingValue === undefined ||
                        this.state.currentPendingValue === null ||
                        this.state.currentPendingValue.length <= 0) &&
                        this.state.currentPendingValueValidIndex < 0))) {
                    this.setState({
                        isOpen: !isOpen
                    });
                }
                // Allow TAB to propigate
                if (ev.which === 9 /* tab */) {
                    return;
                }
                break;
            case 9 /* tab */:
                // On enter submit the pending value
                this._submitPendingValue();
                // If we are not allowing freeform
                // or the comboBox is open, flip the open state
                if (isOpen) {
                    this._setOpenStateAndFocusOnClose(!isOpen, false /* focusInputAfterClose */);
                }
                // Allow TAB to propigate
                return;
            case 27 /* escape */:
                // reset the selected index
                this._resetSelectedIndex();
                // Close the menu if opened
                if (isOpen) {
                    this.setState({
                        isOpen: false
                    });
                }
                else {
                    return;
                }
                break;
            case 38 /* up */:
                // if we are in clearAll state (e.g. the user as hovering
                // and has since mousedOut of the menu items),
                // go to the last index
                if (currentPendingValueValidIndexOnHover === HoverStatus.clearAll) {
                    index = this.state.currentOptions.length;
                }
                // Go to the previous option
                this._setPendingInfoFromIndexAndDirection(index, SearchDirection.backward);
                break;
            case 40 /* down */:
                // Expand the comboBox on ALT + DownArrow
                if (ev.altKey || ev.metaKey) {
                    this._setOpenStateAndFocusOnClose(true /* isOpen */, true /* focusInputAfterClose */);
                }
                else {
                    // if we are in clearAll state (e.g. the user as hovering
                    // and has since mousedOut of the menu items),
                    // go to the first index
                    if (currentPendingValueValidIndexOnHover === HoverStatus.clearAll) {
                        index = -1;
                    }
                    // Got to the next option
                    this._setPendingInfoFromIndexAndDirection(index, SearchDirection.forward);
                }
                break;
            case 36 /* home */:
            case 35 /* end */:
                if (allowFreeform) {
                    return;
                }
                // Set the initial values to respond to HOME
                // which goes to the first selectable option
                index = -1;
                var directionToSearch = SearchDirection.forward;
                // If end, update the values to respond to END
                // which goes to the last selectable option
                if (ev.which === 35 /* end */) {
                    index = currentOptions.length;
                    directionToSearch = SearchDirection.backward;
                }
                this._setPendingInfoFromIndexAndDirection(index, directionToSearch);
                break;
            case 32 /* space */:
                // event handled in _onComboBoxKeyUp
                if (!allowFreeform && autoComplete === 'off') {
                    break;
                }
            default:
                // are we processing a function key? if so bail out
                if (ev.which >= 112 /* F1 */ && ev.which <= 123 /* F12 */) {
                    return;
                }
                // If we get here and we got either and ALT key
                // or meta key and we are current open, let's close the menu
                if ((ev.altKey || ev.metaKey) && isOpen) {
                    this._setOpenStateAndFocusOnClose(!isOpen, true /* focusInputAfterClose */);
                }
                // If we are not allowing freeform and
                // allowing autoComplete, handle the input here
                // since we have marked the input as readonly
                if (!allowFreeform && autoComplete === 'on') {
                    this._onInputChange(String.fromCharCode(ev.which));
                    break;
                }
                // allow the key to propigate by default
                return;
        }
        ev.stopPropagation();
        ev.preventDefault();
    };
    /**
     * Handle keyup on the input
     * @param ev - the keyboard event that was fired
     */
    ComboBox.prototype._onInputKeyUp = function (ev) {
        var _a = this.props, disabled = _a.disabled, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
        if (disabled) {
            this._handleInputWhenDisabled(ev);
            return;
        }
        switch (ev.which) {
            case 32 /* space */:
                // If we are not allowing freeform and are not autoComplete
                // make space expand/collapse the comboBox
                // and allow the event to propagate
                if (!allowFreeform && autoComplete === 'off') {
                    var isOpen = this.state.isOpen;
                    this._setOpenStateAndFocusOnClose(!isOpen, !!isOpen);
                    return;
                }
                break;
            default:
                return;
        }
        ev.stopPropagation();
        ev.preventDefault();
    };
    ComboBox.prototype._onOptionMouseEnter = function (index) {
        if (!this._isScrollIdle) {
            return;
        }
        this.setState({
            currentPendingValueValidIndexOnHover: index
        });
    };
    ComboBox.prototype._onOptionMouseMove = function (index) {
        if (!this._isScrollIdle || this.state.currentPendingValueValidIndexOnHover === index) {
            return;
        }
        this.setState({
            currentPendingValueValidIndexOnHover: index
        });
    };
    /**
     * Handle dismissing the menu and
     * eating the required key event when disabled
     * @param ev - the keyboard event that was fired
     */
    ComboBox.prototype._handleInputWhenDisabled = function (ev) {
        // If we are disabled, close the menu (if needed)
        // and eat all keystokes other than TAB or ESC
        if (this.props.disabled) {
            if (this.state.isOpen) {
                this.setState({ isOpen: false });
            }
            // When disabled stop propagation and prevent default
            // of the event unless we have a tab, escape, or function key
            if (ev !== null &&
                ev.which !== 9 /* tab */ &&
                ev.which !== 27 /* escape */ &&
                (ev.which < 112 /* F1 */ || ev.which > 123 /* F12 */)) {
                ev.stopPropagation();
                ev.preventDefault();
            }
        }
    };
    /**
     * Click handler for the button of the comboBox
     * and the input when not allowing freeform. This
     * toggles the expand/collapse state of the comboBox (if enbled)
     */
    ComboBox.prototype._onComboBoxClick = function () {
        var disabled = this.props.disabled;
        var isOpen = this.state.isOpen;
        if (!disabled) {
            this._setOpenStateAndFocusOnClose(!isOpen, false /* focusInputAfterClose */);
        }
    };
    /**
     * Click handler for the autofill.
     */
    ComboBox.prototype._onAutofillClick = function () {
        if (this.props.allowFreeform) {
            this.focus(this.state.isOpen);
        }
        else {
            this._onComboBoxClick();
        }
    };
    /**
   * Get the styles for the current option.
   * @param item Item props for the current option
   */
    ComboBox.prototype._getCaretButtonStyles = function () {
        var customCaretDownButtonStyles = this.props.caretDownButtonStyles;
        return getCaretDownButtonStyles(this.props.theme, customCaretDownButtonStyles);
    };
    /**
     * Get the styles for the current option.
     * @param item Item props for the current option
     */
    ComboBox.prototype._getCurrentOptionStyles = function (item) {
        var customStylesForAllOptions = this.props.comboBoxOptionStyles;
        var customStylesForCurrentOption = item.styles;
        return getOptionStyles(this.props.theme, customStylesForAllOptions, customStylesForCurrentOption);
    };
    /**
     * Get the aria-activedescendant value for the comboxbox.
     * @returns the id of the current focused combo item, otherwise the id of the currently selected element, null otherwise
     */
    ComboBox.prototype._getAriaActiveDescentValue = function () {
        var descendantText = (this.state.isOpen && this.state.selectedIndex >= 0 ? (this._id + '-list' + this.state.selectedIndex) : null);
        if (this.state.isOpen && this.state.focused && this.state.currentPendingValueValidIndex !== -1) {
            descendantText = (this._id + '-list' + this.state.currentPendingValueValidIndex);
        }
        return descendantText;
    };
    /**
    * Get the aria autocomplete value for the Combobox
    * @returns 'inline' if auto-complete automatically dynamic, 'both' if we have a list of possible values to pick from and can
    * dynamically populate input, and 'none' if auto-complete is not enabled as we can't give user inputs.
    */
    ComboBox.prototype._getAriaAutoCompleteValue = function () {
        var autoComplete = !this.props.disabled && this.props.autoComplete === 'on';
        return autoComplete ? (this.props.allowFreeform ? 'inline' : 'both') : 'none';
    };
    ComboBox.defaultProps = {
        options: [],
        allowFreeform: false,
        autoComplete: 'on',
        buttonIconProps: { iconName: 'ChevronDown' }
    };
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "focus", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "dismissMenu", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onUpdateValueInAutofillWillReceiveProps", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onShouldSelectFullInputValueInAutofillComponentDidUpdate", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_getVisibleValue", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onInputChange", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_select", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onResolveOptions", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onBlur", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onRenderContainer", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onRenderList", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onRenderItem", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onRenderLowerContent", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_renderOption", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onScroll", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onRenderOption", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onDismiss", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onInputKeyDown", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onInputKeyUp", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onComboBoxClick", null);
    tslib_1.__decorate([
        autobind
    ], ComboBox.prototype, "_onAutofillClick", null);
    ComboBox = tslib_1.__decorate([
        customizable('ComboBox', ['theme'])
    ], ComboBox);
    return ComboBox;
}(BaseComponent));
export { ComboBox };
//# sourceMappingURL=ComboBox.js.map