SOURCE CODE: Uize.Widget.AutoSuggest

VIEW REFERENCE

/*______________
|       ______  |   U I Z E    J A V A S C R I P T    F R A M E W O R K
|     /      /  |   ---------------------------------------------------
|    /    O /   |    MODULE : Uize.Widget.AutoSuggest Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://uize.com
| /____/ /__/_| | COPYRIGHT : (c)2011-2014 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 5
  codeCompleteness: 100
  docCompleteness: 100
*/

/*?
  Introduction
    The =Uize.Widget.AutoSuggest= widget manages requesting and display a set of selectable suggestions based on user input into a textbox and supports fine-grained specification of how suggestions are displayed.

    Queries are normalized before requesting suggestions: whitespaces are trimmed.

    When =querySeparators= are specified, text between separators are treated as independent queries.

    The simplest way to specify how suggestions are shown are to set the =highlightMode= and the =cssClass*= state properties.

    For more advanced customization, the =optionDataAdapter=, =optionsWidgetClass=, and =optionsWidgetProperties= state properties allow for fine-grained control over the widgets managing the suggestions.

    *DEVELOPERS:* `Vinson Chuong`, original code contributed by `Zazzle Inc.`

    Issues and Notes
    - Ignoring typeRequestDelay when pasting relies on the onpaste event, which is not supported in Opera.
    - We use min-width to keep the width of the suggestions palette at least the width of the textbox, which doesn't work in IE6.
    - If a suggestion is too long, it overflows the textbox (the last letters and the caret are not shown).
    - Positioning of the palette may be off by varying amounts in different browsers.
*/

Uize.module({
  name: 'Uize.Widget.AutoSuggest',
  superclass: 'Uize.Widget.FormElement.Text',
  required: [
    'Uize.Data.NameValueRecords',
    'Uize.Node',
    'Uize.Dom.Event',
    'Uize.Str.Trim',
    'Uize.Widget.Options.Selector'
  ],
  builder: function (_superclass) {
    'use strict';

    var
      /*** Variables for Scruncher Optimization ***/
        _true = true,
        _false = false,
        _null = null,
        _undefined,
        _emptyString = '',
        _Uize = Uize,
        _Uize_Data = _Uize.Data,
        _Uize_Node = _Uize.Node,
        _Uize_Dom_Event = _Uize.Dom.Event,
        _trim = Uize.Str.Trim.trim,
        _supportsPlaceholder = typeof document != 'undefined'
          && 'placeholder' in document.createElement('input'),

      /*** General Variables ***/
        _highlightModes = { none: 1, query: 1, remaining: 1 }
    ;

    /*** Private Helper Functions ***/
      // Returns a conformer which constrains values to be atleast the given minimum
      function _constrainAtLeast(_min) {
        return function (_value) { return Uize.constrain(_value, _min, Infinity) };
      }

      // Returns x (mod y) in modular arithmetic
      function _mod(_x, _y) { return ((_x % _y) + _y) % _y }

      // Allows for delayed wiring of suggestions widget incase suggestions are never requested
      function _addAndWireSuggestions (m) {
        var
          _cssClassSelected = m._cssClassSelected,
          _suggestions = m.addChild(
            'suggestions',
            m._optionsWidgetClass || _Uize.Widget.Options.Selector,
            _Uize.copyInto(
              {
                built: _false,
                html: _true,
                optionWidgetProperties: {
                  cssClassActive:_cssClassSelected,
                  cssClassSelected:_cssClassSelected,
                  cssClassTentativeSelected:_cssClassSelected
                },
                values: []
              },
              m._optionsWidgetProperties
            )
          )
        ;

        _suggestions.wire({
          // When a suggestion is highlighted (eg hovered over), it is shown in the textbox.
          // When no suggestions are highlighted, the original query will be shown.
          'Changed.tentativeValue': function () {
            m._showOnHover && m._suggestionHoverHandler(_suggestions.get('tentativeValue'));
          },

          // When a suggestion is clicked, the value of this widget is set to the suggestion
          // and we submit.
          'Option Event': function (_event) {
            if (_event.childEvent.name === 'Click') {
              m._showOnHover || m._suggestionHoverHandler(_suggestions.get('tentativeValue'));
              m._canUpdateLastTypedQuery = _false;
              m.set('focused', _true);
              _fireSuggestionSelected(m, _event.childEvent.source);
            }
          }
        });

        _suggestions.wireUi();

        return _suggestions;
      }

      function _fireSuggestionSelected (m, _option) {
        m.fire({
          name: 'Suggestion Selected',
          option: _option
        });
        /*?
        Instance Events
        Suggestion Selected
        This event fires when a suggestion is selected (click or enter).

        When this event fires, the event object will have an =option= property whose value is the option widget representing the suggestion.
        */
        m._typedQueryTokenInfo = m._tokenInfo;
        m.children.suggestions.set({
          tentativeValue: _null,
          tentativeValueNo: -1,
          value: _null,
          values: []
        });
        _updateSuggestionsPalette(m);
      }

      function _getDisplay (_tokenInfo, _suggestion) {
        var
          _tokens = _tokenInfo.tokens.concat(),
          _index = _tokenInfo.tokenIndex,
          _text
        ;

        if (_suggestion) {
          _tokens[_index] = _suggestion;
          _text = _tokens.join('');
        } else
          _text = _tokenInfo.query;

        return {
          text: _text,
          position: _tokens.splice(0, _index + 1).join('').length
        };
      }

      function _getNormalizedQuery (m, _tokenInfo) {
        var _normalizedQuery = _tokenInfo && _trim(_tokenInfo.tokens.concat()[_tokenInfo.tokenIndex]).replace(/\s+/g, ' ');
        return _tokenInfo ?
          (!_supportsPlaceholder && _normalizedQuery == m.get('defaultValue') ?
            _emptyString :
            _normalizedQuery)
          : _emptyString
        ;
      }

      function _getTokenInfo (m, _input, _position) {
        var
          _separators = m._querySeparators,
          _quotes = m._queryQuotes,
          _inputLength = _input.length,
          _queryIndex = _position,
          _tokens = [],
          _index = -1,
          _curToken = _emptyString,
          _curChar = _emptyString,
          _inQuotes = _false,
          _curQuotes = _emptyString,
          _tokenIndex = 0
        ;

        if (_separators) {
          _separators = _Uize.lookup(_Uize.isArray(_separators) ? _separators : [_separators]);
          _quotes =
            _quotes ?
              _Uize_Data.NameValueRecords.toHash(
                _Uize.isArray(_quotes) ?
                  typeof _quotes[0] === 'object' ?
                    _quotes :
                    _Uize.map(_quotes, function (_quote) { return { open: _quote, close: _quote} }) :
                  typeof _quotes === 'object' ?
                    [_quotes] :
                    [{ open: _quotes, close: _quotes}],
                'open',
                'close'
              ) :
              {}
          ;

          while (++_index < _inputLength) {
            _curChar = _input[_index];
            if (!_inQuotes && (_curQuotes = _quotes[_curChar])) {
              _inQuotes = _true;
              _curToken += _curChar;
            } else if (_inQuotes && _curChar === _curQuotes) {
              _inQuotes = _false;
              _curToken += _curChar;
            } else if (!_inQuotes && _separators[_curChar]) {
              _tokens.push(_curToken);
              _tokens.push(_curChar);
              _curToken = _emptyString;
            } else {
              _curToken += _curChar;
            }
          }
          if (_curToken || !_tokens.length) _tokens.push(_curToken);
        } else
          _tokens.push(_input);

        if (_position == -1)
          _tokenIndex = -1;
        else
          while (_tokenIndex < _tokens.length - 1 && (_position -= _tokens[_tokenIndex].length) > 0) _tokenIndex++;

        return {
          query: _input,
          queryIndex: _queryIndex,
          tokens: _tokens,
          tokenIndex: _tokenIndex
        };
      }

      function _handleKeyboardControl (m, _domEvent) {
        var _suggestions = m.children.suggestions;

        if (m.isWired) {
          if (
            m._allowKeypress &&
            _suggestions &&
            m.getNodeStyle('suggestionsPalette', 'display') != 'none'
          ) {
            // "hover" over suggestions via up/down arrows
            if (_Uize_Dom_Event.isKeyUpArrow(_domEvent) || _Uize_Dom_Event.isKeyDownArrow(_domEvent)) {
              var
                _curNumSuggestions = _suggestions.get('values').length,
                _increment =
                  _Uize_Dom_Event.isKeyUpArrow(_domEvent) ? -1 :
                  _Uize_Dom_Event.isKeyDownArrow(_domEvent) ? 1 :
                  0,
                _curValueNo = _suggestions.get('tentativeValueNo'),
                _curSuggestion = _suggestions.children['option' + _curValueNo],
                _nextSuggestion = _suggestions.children[
                  'option' +
                  (_mod(_curValueNo + 1 + _increment, _curNumSuggestions + 1) - 1)
                ]
              ;
              if (_increment && _curSuggestion) {
                _curSuggestion.set('state', _emptyString);
                _curSuggestion.fire('Out');
              }
              if (_increment && _nextSuggestion) {
                _nextSuggestion.set('state', 'over');
                _nextSuggestion.fire('Over');
              }
              m._showOnMouseover ||
                  m._suggestionHoverHandler(_suggestions.get('tentativeValue'));
              // select a "hovered over" suggestion using enter or tab
            } else if (
              _suggestions.get('tentativeValue') &&
              (
                _Uize_Dom_Event.isKeyEnter(_domEvent) ||
                _Uize_Dom_Event.isKeyTab(_domEvent)
              )
            ) {
              _fireSuggestionSelected(m, _suggestions.children['option' + _suggestions.get('tentativeValueNo')]);
              // select first suggestion on tab
            } else if (_Uize_Dom_Event.isKeyTab(_domEvent)) {
              var _firstSuggestion = _suggestions.children['option0'];
              _firstSuggestion.set('state', 'over');
              _firstSuggestion.fire('Over');
              _fireSuggestionSelected(m, _firstSuggestion);
            }
          }
        }
      }

      function _updateSuggestions (m) {
        function _highlight(_text) { return '' + _text + '' }

        var
          _children = m.children,
          _suggestions = _children.suggestions,
          _normalizedQuery = _getNormalizedQuery(m, m._typedQueryTokenInfo),
          _defaultValue = m.get('defaultValue')
        ;

        m._canUpdateLastTypedQuery &&
          m.set({ lastTypedQuery: _normalizedQuery })
        ;

        if (
          (_normalizedQuery != _defaultValue || (_normalizedQuery == _defaultValue && !_defaultValue)) &&
          _normalizedQuery.length >= m._numCharsBeforeSuggest &&
          m._numSuggestions &&
          m._serviceUrl != _undefined
        ) {
          m.ajax(
            _Uize.copyInto(
              _Uize.pairUp(
                'serviceUrl', m._serviceUrl,
                m._serviceQueryParamName, _normalizedQuery,
                m._serviceNumSuggestionsParamName, m._numSuggestions
              ),
              m._additionalAutoSuggestParams || {}
            ),
            {
              cache: 'memory',
              callbackSuccess: function (_response) {
                (_suggestions || _addAndWireSuggestions(m)).set({
                  tentativeValue: _null,
                  tentativeValueNo: -1,
                  values: _Uize.map(
                    m._responseAdapter(_normalizedQuery, _response),
                    function (_suggestion) {
                      var
                        _term = _suggestion.fullWord,
                        _highlightMode = m._highlightMode,
                        _formattedTerm =
                          _highlightMode === 'none' ? _term :
                          _highlightMode === 'query' ? _suggestion.prefix + _highlight(_normalizedQuery) + _suggestion.suffix :
                          _suggestion.fullWord.indexOf(_normalizedQuery) === 0 ? _highlight(_suggestion.prefix) + _normalizedQuery + _highlight(_suggestion.suffix):
                          _highlight(_suggestion.fullWord)
                            /* NOTE:
                              Hightlight the whole word if the word doesn't start with the normalizedQuery. This is the case when the normalized query is spelled wrong and the suggestion comes from the spell checker
                            */
                      ;
                      return m._optionDataAdapter(_term, _formattedTerm);
                    }
                  )
                });
                _updateSuggestionsPalette(m);
              }
            }
          );
        } else if (_suggestions) {
          _suggestions.set({
            tentativeValue: _null,
            tentativeValueNo: -1,
            values: []
          });
          _updateSuggestionsPalette(m);
        }
      }

      function _updateSuggestionsPalette (m) {
        if (m.isWired) {
          var
            _inputNode = m.getNode('input'),
            _suggestionsPaletteNode = m.getNode('suggestionsPalette'),
            _focused = m.get('focused'),
            _suggestions = m.children.suggestions,
            _hasSuggestions = _suggestions && _suggestions.get('values').length
          ;

          if (_focused && _hasSuggestions) {
            m.displayNode('trending', !m._lastTypedQuery);
            // The palette must have display:true to be positioned
            m.showNode(_suggestionsPaletteNode, _false);
            m.displayNode(_suggestionsPaletteNode);
            if (m.get('autoPositionSuggestionsPalette')) {
              _Uize_Node.setAbsPosAdjacentTo(_suggestionsPaletteNode, _inputNode, 'y');
              // We want the palette to be at least as wide as the input and as wide as needed to display every
              // suggestion.
              m.setNodeStyle(
                _suggestionsPaletteNode,
                { minWidth: _Uize_Node.getDimensions(_inputNode).width }
              );
            }
            m.showNode(_suggestionsPaletteNode);
          } else if (m.getNodeStyle('suggestionsPalette', 'display') != 'none') {
            // When the input loses focus, the following line is executed. When a suggestion is clicked,
            // the input loses focus. The blur event will fire before the Click event. We must wait until
            // the Click event is fired before setting display:none on the palette.
            setTimeout(
              function () {
                m.displayNode(_suggestionsPaletteNode, _false);
                _suggestions && _suggestions.set({
                  tentativeValue: _null,
                  tentativeValueNo: -1,
                  value: _null,
                  values: []
                });
              },
              200
            );
          }
        }
      }

    return _superclass.subclass ({
      omegastructor:function () {
        var
          m = this,
          _preFocusQuery = ''
        ;

        /*** Private Instance Variables ***/
        m._ignoreNextRequestDelay = _false;
        m._preventRequests = _false;
        m._typedQueryTokenInfo = _null;
        m._tokenInfo = _null;
        m._canUpdateLastTypedQuery = _true;
        m._suggestionHoverHandler = function (_suggestion) {
          var
            _displayInfo = _suggestion ?
              _getDisplay(m._tokenInfo, _suggestion) :
              _getDisplay(m._typedQueryTokenInfo)
          ;
          m._preventRequests = _true;
          if (_displayInfo.text === m + '') m.set('value', '');
          m.set('value', _displayInfo.text);
          m._preventRequests = _false;
          m.setCaretPosition(_displayInfo.position);
          m._tokenInfo = _getTokenInfo(m,_displayInfo.text, _displayInfo.position);
        };

        /*** Wire Events on Inherited Widget ***/
        // hide suggestions when the caret is positioned over a different subquery
        function _checkSubqueryChange(_oldTokenInfo, _newTokenInfo) {
          if (m._querySeparators) {
            if (_oldTokenInfo.tokenIndex != _newTokenInfo.tokenIndex) {
              m._typedQueryTokenInfo = _newTokenInfo;
              m.children.suggestions.set({
                tentativeValue: _null,
                tentativeValueNo: -1,
                value: _null,
                values: []
              });
              _updateSuggestionsPalette(m);
            }
          }
        }

        m.wire({
          // Maintain the semantics of the Cancel event from Uize.Widget.FormElement:
          // to restore the pre-focus value of the input
          Cancel: function () {
            m._preventRequests = _true;
            m.set('value', _preFocusQuery);
            m._preventRequests = _false;
          },
          'Changed.focused': function () {
            if (m.get('focused')) {
              _preFocusQuery = m + '';
              m._typedQueryTokenInfo = m._tokenInfo = _getTokenInfo(
                m,
                m.get('tentativeValue'),
                m.getCaretPosition()
              );
              _updateSuggestions(m);
            }
            _updateSuggestionsPalette(m);
          },

          // _preventRequests is set to true whenever we want to distinguish text input by the
          // widget and by the user. When the widget changes the value of the input (eg to show
          // the currently selected/hovered suggestion), we don't want new suggestions to be
          // requested.
          'Changed.tentativeValue': function () {
            if (!m._preventRequests && m.get('focused')) {
              clearTimeout(m._typeSuggestTimeout);
              if (m._ignoreNextRequestDelay) {
                m._ignoreNextRequestDelay = _false;
                _updateSuggestions(m);
              } else
                m._typeSuggestTimeout = setTimeout(
                  function () { _updateSuggestions(m) },
                  m._typeSuggestDelay
                );
            }
          },

          'Key Up': function (_event) {
            var
              _oldTokenInfo = m._tokenInfo,
              _domEvent = _event.domEvent
            ;

            // re-tokenize input, taking care that it is done at-most once per user-action
            if (_Uize_Dom_Event.isKeyEscape(_domEvent)) {
              m._typedQueryTokenInfo =
                m._tokenInfo =
                _getTokenInfo(m,_preFocusQuery, -1);
            } else if (
              !(
                m.isWired &&
                m._allowKeypress &&
                m.getNodeStyle('suggestionsPalette', 'display') != 'none' &&
                (
                  _Uize_Dom_Event.isKeyUpArrow(_domEvent) ||
                  _Uize_Dom_Event.isKeyDownArrow(_domEvent)
                )
              )
            ) {
              m._tokenInfo = _getTokenInfo(
                m,
                m.get('tentativeValue'),
                m.getCaretPosition()
              );
              if (!m._preventRequests && m.get('focused'))
                m._typedQueryTokenInfo = m._tokenInfo;
            }

            // hide suggestions when caret moves into different subquery
            _oldTokenInfo && _checkSubqueryChange(_oldTokenInfo, m._tokenInfo);

            // handle keystroke control semantics
            _handleKeyboardControl(m,_event.domEvent);
          },

          // hide suggestions when caret moves into different subquery
          // Click event fires when the textbox is clicked on
          Click: function () {
            var _oldTokenInfo = m._tokenInfo;
            m._tokenInfo = _getTokenInfo(
              m,
              m.get('tentativeValue'),
              m.getCaretPosition()
            );

            _oldTokenInfo && _checkSubqueryChange(_oldTokenInfo, m._tokenInfo);
          },

          Ok: function () { m.set('focused', _false) }
        });
      },

      instanceMethods:{
        // prevents the Ok event from being fired on Enter when a suggestion is hovered
        // overrides Uize.Widget.FormElement.Text
        fireOkOnEnter:function () {
          var _suggestions = this.children.suggestions;
          return !(_suggestions && _suggestions.get('tentativeValue'));
        },

        updateUi:function () {
          var m = this;

          if (m.isWired) {
            _updateSuggestionsPalette(m);

            _superclass.doMy (m,'updateUi');
          }
        },

        wireUi:function () {
          var
            m = this,
            _docBody = document.body,
            _suggestionsPaletteNode = m.getNode('suggestionsPalette')
          ;

          if (!m.isWired) {
            m.wireNode(
              'input',
              {
                // handle pasting (does not work in Opera)
                // onpaste fires before Changed.tentativeValue
                paste: function () { m._ignoreNextRequestDelay = _true },

                // prevent:
                // - cursor movement on input via up/down arrow
                // - form submission when a suggestion is selected with the Enter key
                // - changing form elements on Tab when a suggestion is available
                keydown: function (_domEvent) {
                  var _suggestions = m.children.suggestions;
                  m._allowKeypress &&
                    (
                      _Uize_Dom_Event.isKeyUpArrow(_domEvent) ||
                      _Uize_Dom_Event.isKeyDownArrow(_domEvent) ||
                      (
                        _Uize_Dom_Event.isKeyTab(_domEvent) &&
                        _suggestions &&
                        _suggestions.get('values').length
                      ) ||
                      (
                        _Uize_Dom_Event.isKeyEnter(_domEvent) &&
                        _suggestions &&
                        _suggestions.get('tentativeValue')
                      )
                    ) &&
                    _Uize_Dom_Event.preventDefault(_domEvent)
                  ;
                }
              }
            );

            // move suggestions palette to root
            if (_suggestionsPaletteNode && _suggestionsPaletteNode.parentNode != _docBody) {
              _docBody.insertBefore(_suggestionsPaletteNode, _docBody.childNodes[0]);
              m.setNodeStyle(
                _suggestionsPaletteNode,
                {
                  display: 'none',
                  position: 'absolute',
                  zIndex: 10000,
                  left: '',
                  top: '',
                  right: '',
                  bottom: ''
                }
              );
            }

            // disable browser autocomplete
            m.setNodeProperties('input', { autocomplete: 'off' });

            _superclass.doMy (m,'wireUi');
          }
        }
      },

      stateProperties:{
        _additionalAutoSuggestParams:{
          name: 'additionalAutoSuggestParams',
          value: {}
        },
        _allowKeypress: {
          name: 'allowKeypress',
          value: _true
          /*?
            State Properties
              allowKeypress
                A boolean, specifying whether suggestions can be selected via keyboard control.

                NOTES
                - the initial value is =true=
          */
        },
        _autoPositionSuggestionsPalette: {
          name: 'autoPositionSuggestionsPalette',
          value: true
          /*?
            State Properties
              autoPositionSuggestionsPalette
                Set this to false to use the default CSS positioning for the suggestions palette.
          */
        },
        _cssClassHighlight: {
          name: 'cssClassHighlight',
          value: 'suggestionHighlight'
          /*?
            State Properties
              cssClassHighlight
                A string, indicating the CSS class used for highlighting (see the =highlightMode= property) specified portions of the suggestions.

                NOTES
                - the initial value is ='suggestionHighlight'=
          */
        },
        _cssClassSelected:{
          name:'cssClassSelected',
          value:'selectedSuggestion'
          /*?
            State Properties
              cssClassSelected
                A string, indicating the CSS class used for the (tentatively) selected suggestion

                NOTES
                - the initial value is ='selectedSuggestion'=
          */
        },
        _highlightMode: {
          name: 'highlightMode',
          conformer: function (_value) { return _highlightModes[_value] ? _value : 'query' },
          value: 'query'
          /*?
            State Properties
              highlightMode
                A string, specifying the portion of each suggestion that is highlighted (wrapped in a span with class =cssClassHighlight=).

                NOTES
                - the value must be one of ='none'=, ='query'=, or ='remaining'=
                - the initial value is ='query'=
          */
        },
        _lastTypedQuery:{
          name:'lastTypedQuery'
          /*?
            State Properties
              lastTypedQuery
                A read only string, specifies the last query used for auto suggest

                NOTES
                - the initial value is =undefined=
                - Read Only
          */
        },
        _numCharsBeforeSuggest: {
          name: 'numCharsBeforeSuggest',
          conformer: _constrainAtLeast(0),
          value: 1
          /*?
            State Properties
              numCharsBeforeSuggest
                An integer, specifying the minimum number of characters input before suggestions are requested.

                The primary use of such a minimum is to increase the relevance of the returned suggestions.

                NOTES
                - the value must be at least 0
                - the initial value is =1=
          */
        },
        _numSuggestions: {
          name: 'numSuggestions',
          conformer: _constrainAtLeast(0),
          value: 10
          /*?
            State Properties
              numSuggestions
                An integer, specifying the maxinum number of suggestions requested and displayed.

                NOTES
                - the value must be at least =0=
                - setting the value to =0= completely disables suggestions
                - the initial value is =10=
          */
        },
        _optionDataAdapter: {
          name: 'optionDataAdapter',
          value: function (_term, _formattedTerm) {
            return {
              name: _term,
              valueDetails: {
                name: _term,
                displayName: _formattedTerm
              }
            };
          }
          /*?
            State Properties
              optionDataAdapter
                A function which maps a suggestion and its formatted form to a form accepted by the ==values== property of the ==optionsWidgetClass==.

                NOTES
                - the initial value is a function which maps the suggestion and formatted suggestion into a form accepted by =Uize.Widget.Options.Selector=.
          */
        },
        _optionsWidgetClass: 'optionsWidgetClass',
        /*?
          State Properties
            optionsWidgetClass
              An object reference to a widget class which is used for the suggestions.

              By default, when no class is specified, =Uize.Widget.Options.Selector= is used.

              NOTES
              - the initial value is =undefined=
        */
        _optionsWidgetProperties: 'optionsWidgetProperties',
        /*?
          State Properties
            optionsWidgetProperties
              An object, specifying values for state properties that is used when creating the options widget.

              NOTES
              - see the companion =optionsWidgetClass= state property
              - the initial value is =undefined=
        */
        _queryQuotes: 'queryQuotes',
        /*?
          State Properties
            queryQuotes
              A string, object, array of strings, or array of objects, used to enclose substrings in which separators are ignored.

              Accepted values include =quoteSTR=, =[quoteSTR1, quoteSTR2, ...]=, ={open:openQuoteSTR, close:closeQuoteSTR}=, and =[{open:openQuoteSTR1, close:closeQuoteSTR1}, {open:openQuoteSTR2, close:closeQuoteSTR2}]=

              A value of =undefined=, =null=, or ==''== disables this feature.

              NOTES
              - the initial value is =undefined=
              - this property is only used when =querySeparators= is defined
        */
        _querySeparators: 'querySeparators',
        /*?
          State Properties
            querySeparators
              A character (string) or array of characters, used to split and distinguish independent subqueries.

              Accepted valeus include =separatorSTR= and =[separatorSTR1, separatorSTR2, ...]=.

              For example, for an input of ='foo,bar,baz'= with separator =','=, the subqueries would be ='foo'=, ='bar'=, and ='baz'=. An input of ='foo,bar|baz'= with separators =[',', '|']= would yield the same subqueries.

              A value of =undefined=, =null=, or ==''== disables this feature.

              NOTES
              - the initial value is =undefined=
        */
        _responseAdapter: {
          name: 'responseAdapter',
          value: function (_normalizedQuery, _response) {
            return _Uize.map(
              _response,
              function (_term) {
                return {
                  prefix: '',
                  suffix: _term.substr(_normalizedQuery.length),
                  fullWord: _term

                };
              }
            );
          }
          /*?
            State Properties
              responseAdapter
                A function which maps the normalized query and the response from the service used to request suggestions to an array of of objects containing a =prefix= and a =suffix=.

                NOTES
                - the initial value is a function which assumes that the service returns an array of suggestion strings (eg =['foo','bar','baz']=)
          */
        },
        _serviceUrl: 'serviceUrl',
        /*?
        State Properties
          serviceUrl
            A string specifying the URL of the service from which suggestions are requested.

            This property is required to be set.

            NOTES
            - the initial value is =undefined=
        */
        _serviceQueryParamName: {
          name: 'serviceQueryParamName',
          value: 'q'
          /*?
            State Properties
              serviceQueryParamName
                A string specifying the parameter name used by the service (provided at =serviceUrl=) to denote the query.

                NOTES
                - the initial value is ='q'=
          */
        },
        _serviceNumSuggestionsParamName: {
          name: 'serviceNumSuggestionsParamName',
          value: 'num'
          /*?
            State Properties
              serviceNumSuggestionsParamName
                A string specifying the parameter name used by the service (provided at =serviceUrl=) to denote the number of suggestions to return.

                NOTES
                - the initial value is ='num'=
                - the number of suggestions to request is specified by =numSuggestions=
          */
        },
        _showOnMouseover: {
          name: 'showOnMouseover',
          value: _false
          /*?
            State Properties
              showOnMouseover
                A boolean specifying whether on mouseover of a suggestion, the suggestion is shown in the input.

                NOTES
                - the initial value is =false=
                - This feature causes a usability issue. If the user's mouse is over where suggestions would appear, suggestions would be automatically selected and the contents of the input updated, while the user is still typing.
          */
        },
        _typeSuggestDelay: {
          name: 'typeSuggestDelay',
          conformer: _constrainAtLeast(0),
          value: 10
          /*?
            State Properties
              typeSuggestDelay
                An integer, specifying the amount of time after the user stops typing before suggestions are requested.

                The primary use of such a delay is to increase responsiveness by minimizing the number of wasted requests sent.

                NOTES
                - the value must be at least =0=
                - the initial value is =0=
          */
        }
      }
    });
  }
});