Version 3.18.1
Show:

File: autocomplete/js/autocomplete-sources.js

            /**
            Mixes support for JSONP and YQL result sources into AutoCompleteBase.
            
            @module autocomplete
            @submodule autocomplete-sources
            **/
            
            var ACBase = Y.AutoCompleteBase,
                Lang   = Y.Lang,
            
                _SOURCE_SUCCESS = '_sourceSuccess',
            
                MAX_RESULTS         = 'maxResults',
                REQUEST_TEMPLATE    = 'requestTemplate',
                RESULT_LIST_LOCATOR = 'resultListLocator';
            
            // Add prototype properties and methods to AutoCompleteBase.
            Y.mix(ACBase.prototype, {
                /**
                Regular expression used to determine whether a String source is a YQL query.
            
                @property _YQL_SOURCE_REGEX
                @type RegExp
                @protected
                @for AutoCompleteBase
                **/
                _YQL_SOURCE_REGEX: /^(?:select|set|use)\s+/i,
            
                /**
                Runs before AutoCompleteBase's `_createObjectSource()` method and augments
                it to support additional object-based source types.
            
                @method _beforeCreateObjectSource
                @param {String} source
                @protected
                @for AutoCompleteBase
                **/
                _beforeCreateObjectSource: function (source) {
                    // If the object is a <select> node, use the options as the result
                    // source.
                    if (source instanceof Y.Node &&
                            source.get('nodeName').toLowerCase() === 'select') {
            
                        return this._createSelectSource(source);
                    }
            
                    // If the object is a JSONPRequest instance, try to use it as a JSONP
                    // source.
                    if (Y.JSONPRequest && source instanceof Y.JSONPRequest) {
                        return this._createJSONPSource(source);
                    }
            
                    // Fall back to a basic object source.
                    return this._createObjectSource(source);
                },
            
                /**
                Creates a DataSource-like object that uses `Y.io` as a source. See the
                `source` attribute for more details.
            
                @method _createIOSource
                @param {String} source URL.
                @return {Object} DataSource-like object.
                @protected
                @for AutoCompleteBase
                **/
                _createIOSource: function (source) {
                    var ioSource = {type: 'io'},
                        that     = this,
                        ioRequest, lastRequest, loading;
            
                    // Private internal _sendRequest method that will be assigned to
                    // ioSource.sendRequest once io-base and json-parse are available.
                    function _sendRequest(request) {
                        var cacheKey = request.request;
            
                        // Return immediately on a cached response.
                        if (that._cache && cacheKey in that._cache) {
                            that[_SOURCE_SUCCESS](that._cache[cacheKey], request);
                            return;
                        }
            
                        // Cancel any outstanding requests.
                        if (ioRequest && ioRequest.isInProgress()) {
                            ioRequest.abort();
                        }
            
                        ioRequest = Y.io(that._getXHRUrl(source, request), {
                            on: {
                                success: function (tid, response) {
                                    var data;
            
                                    try {
                                        data = Y.JSON.parse(response.responseText);
                                    } catch (ex) {
                                        Y.error('JSON parse error', ex);
                                    }
            
                                    if (data) {
                                        that._cache && (that._cache[cacheKey] = data);
                                        that[_SOURCE_SUCCESS](data, request);
                                    }
                                }
                            }
                        });
                    }
            
                    ioSource.sendRequest = function (request) {
                        // Keep track of the most recent request in case there are multiple
                        // requests while we're waiting for the IO module to load. Only the
                        // most recent request will be sent.
                        lastRequest = request;
            
                        if (loading) { return; }
            
                        loading = true;
            
                        // Lazy-load the io-base and json-parse modules if necessary,
                        // then overwrite the sendRequest method to bypass this check in
                        // the future.
                        Y.use('io-base', 'json-parse', function () {
                            ioSource.sendRequest = _sendRequest;
                            _sendRequest(lastRequest);
                        });
                    };
            
                    return ioSource;
                },
            
                /**
                Creates a DataSource-like object that uses the specified JSONPRequest
                instance as a source. See the `source` attribute for more details.
            
                @method _createJSONPSource
                @param {JSONPRequest|String} source URL string or JSONPRequest instance.
                @return {Object} DataSource-like object.
                @protected
                @for AutoCompleteBase
                **/
                _createJSONPSource: function (source) {
                    var jsonpSource = {type: 'jsonp'},
                        that        = this,
                        lastRequest, loading;
            
                    function _sendRequest(request) {
                        var cacheKey = request.request,
                            query    = request.query;
            
                        if (that._cache && cacheKey in that._cache) {
                            that[_SOURCE_SUCCESS](that._cache[cacheKey], request);
                            return;
                        }
            
                        // Hack alert: JSONPRequest currently doesn't support
                        // per-request callbacks, so we're reaching into the protected
                        // _config object to make it happen.
                        //
                        // This limitation is mentioned in the following JSONP
                        // enhancement ticket:
                        //
                        // http://yuilibrary.com/projects/yui3/ticket/2529371
                        source._config.on.success = function (data) {
                            that._cache && (that._cache[cacheKey] = data);
                            that[_SOURCE_SUCCESS](data, request);
                        };
            
                        source.send(query);
                    }
            
                    jsonpSource.sendRequest = function (request) {
                        // Keep track of the most recent request in case there are multiple
                        // requests while we're waiting for the JSONP module to load. Only
                        // the most recent request will be sent.
                        lastRequest = request;
            
                        if (loading) { return; }
            
                        loading = true;
            
                        // Lazy-load the JSONP module if necessary, then overwrite the
                        // sendRequest method to bypass this check in the future.
                        Y.use('jsonp', function () {
                            // Turn the source into a JSONPRequest instance if it isn't
                            // one already.
                            if (!(source instanceof Y.JSONPRequest)) {
                                source = new Y.JSONPRequest(source, {
                                    format: Y.bind(that._jsonpFormatter, that)
                                });
                            }
            
                            jsonpSource.sendRequest = _sendRequest;
                            _sendRequest(lastRequest);
                        });
                    };
            
                    return jsonpSource;
                },
            
                /**
                Creates a DataSource-like object that uses the specified `<select>` node as
                a source.
            
                @method _createSelectSource
                @param {Node} source YUI Node instance wrapping a `<select>` node.
                @return {Object} DataSource-like object.
                @protected
                @for AutoCompleteBase
                **/
                _createSelectSource: function (source) {
                    var that = this;
            
                    return {
                        type: 'select',
                        sendRequest: function (request) {
                            var options = [];
            
                            source.get('options').each(function (option) {
                                options.push({
                                    html    : option.get('innerHTML'),
                                    index   : option.get('index'),
                                    node    : option,
                                    selected: option.get('selected'),
                                    text    : option.get('text'),
                                    value   : option.get('value')
                                });
                            });
            
                            that[_SOURCE_SUCCESS](options, request);
                        }
                    };
                },
            
                /**
                Creates a DataSource-like object that calls the specified  URL or executes
                the specified YQL query for results. If the string starts with "select ",
                "use ", or "set " (case-insensitive), it's assumed to be a YQL query;
                otherwise, it's assumed to be a URL (which may be absolute or relative).
                URLs containing a "{callback}" placeholder are assumed to be JSONP URLs; all
                others will use XHR. See the `source` attribute for more details.
            
                @method _createStringSource
                @param {String} source URL or YQL query.
                @return {Object} DataSource-like object.
                @protected
                @for AutoCompleteBase
                **/
                _createStringSource: function (source) {
                    if (this._YQL_SOURCE_REGEX.test(source)) {
                        // Looks like a YQL query.
                        return this._createYQLSource(source);
                    } else if (source.indexOf('{callback}') !== -1) {
                        // Contains a {callback} param and isn't a YQL query, so it must be
                        // JSONP.
                        return this._createJSONPSource(source);
                    } else {
                        // Not a YQL query or JSONP, so we'll assume it's an XHR URL.
                        return this._createIOSource(source);
                    }
                },
            
                /**
                Creates a DataSource-like object that uses the specified YQL query string to
                create a YQL-based source. See the `source` attribute for details. If no
                `resultListLocator` is defined, this method will set a best-guess locator
                that might work for many typical YQL queries.
            
                @method _createYQLSource
                @param {String} source YQL query.
                @return {Object} DataSource-like object.
                @protected
                @for AutoCompleteBase
                **/
                _createYQLSource: function (source) {
                    var that      = this,
                        yqlSource = {type: 'yql'},
                        lastRequest, loading, yqlRequest;
            
                    if (!that.get(RESULT_LIST_LOCATOR)) {
                        that.set(RESULT_LIST_LOCATOR, that._defaultYQLLocator);
                    }
            
                    function _sendRequest(request) {
                        var query      = request.query,
                            env        = that.get('yqlEnv'),
                            maxResults = that.get(MAX_RESULTS),
                            callback, opts, yqlQuery;
            
                        yqlQuery = Lang.sub(source, {
                            maxResults: maxResults > 0 ? maxResults : 1000,
                            request   : request.request,
                            query     : query
                        });
            
                        if (that._cache && yqlQuery in that._cache) {
                            that[_SOURCE_SUCCESS](that._cache[yqlQuery], request);
                            return;
                        }
            
                        callback = function (data) {
                            that._cache && (that._cache[yqlQuery] = data);
                            that[_SOURCE_SUCCESS](data, request);
                        };
            
                        opts = {proto: that.get('yqlProtocol')};
            
                        // Only create a new YQLRequest instance if this is the
                        // first request. For subsequent requests, we'll reuse the
                        // original instance.
                        if (yqlRequest) {
                            yqlRequest._callback   = callback;
                            yqlRequest._opts       = opts;
                            yqlRequest._params.q   = yqlQuery;
            
                            if (env) {
                                yqlRequest._params.env = env;
                            }
                        } else {
                            yqlRequest = new Y.YQLRequest(yqlQuery, {
                                on: {success: callback},
                                allowCache: false // temp workaround until JSONP has per-URL callback proxies
                            }, env ? {env: env} : null, opts);
                        }
            
                        yqlRequest.send();
                    }
            
                    yqlSource.sendRequest = function (request) {
                        // Keep track of the most recent request in case there are multiple
                        // requests while we're waiting for the YQL module to load. Only the
                        // most recent request will be sent.
                        lastRequest = request;
            
                        if (!loading) {
                            // Lazy-load the YQL module if necessary, then overwrite the
                            // sendRequest method to bypass this check in the future.
                            loading = true;
            
                            Y.use('yql', function () {
                                yqlSource.sendRequest = _sendRequest;
                                _sendRequest(lastRequest);
                            });
                        }
                    };
            
                    return yqlSource;
                },
            
                /**
                Default resultListLocator used when a string-based YQL source is set and the
                implementer hasn't already specified one.
            
                @method _defaultYQLLocator
                @param {Object} response YQL response object.
                @return {Array}
                @protected
                @for AutoCompleteBase
                **/
                _defaultYQLLocator: function (response) {
                    var results = response && response.query && response.query.results,
                        values;
            
                    if (results && Lang.isObject(results)) {
                        // If there's only a single value on YQL's results object, that
                        // value almost certainly contains the array of results we want. If
                        // there are 0 or 2+ values, then the values themselves are most
                        // likely the results we want.
                        values  = Y.Object.values(results) || [];
                        results = values.length === 1 ? values[0] : values;
            
                        if (!Lang.isArray(results)) {
                            results = [results];
                        }
                    } else {
                        results = [];
                    }
            
                    return results;
                },
            
                /**
                Returns a formatted XHR URL based on the specified base _url_, _query_, and
                the current _requestTemplate_ if any.
            
                @method _getXHRUrl
                @param {String} url Base URL.
                @param {Object} request Request object containing `query` and `request`
                  properties.
                @return {String} Formatted URL.
                @protected
                @for AutoCompleteBase
                **/
                _getXHRUrl: function (url, request) {
                    var maxResults = this.get(MAX_RESULTS);
            
                    if (request.query !== request.request) {
                        // Append the request template to the URL.
                        url += request.request;
                    }
            
                    return Lang.sub(url, {
                        maxResults: maxResults > 0 ? maxResults : 1000,
                        query     : encodeURIComponent(request.query)
                    });
                },
            
                /**
                URL formatter passed to `JSONPRequest` instances.
            
                @method _jsonpFormatter
                @param {String} url
                @param {String} proxy
                @param {String} query
                @return {String} Formatted URL
                @protected
                @for AutoCompleteBase
                **/
                _jsonpFormatter: function (url, proxy, query) {
                    var maxResults      = this.get(MAX_RESULTS),
                        requestTemplate = this.get(REQUEST_TEMPLATE);
            
                    if (requestTemplate) {
                        url += requestTemplate(query);
                    }
            
                    return Lang.sub(url, {
                        callback  : proxy,
                        maxResults: maxResults > 0 ? maxResults : 1000,
                        query     : encodeURIComponent(query)
                    });
                }
            });
            
            // Add attributes to AutoCompleteBase.
            Y.mix(ACBase.ATTRS, {
                /**
                YQL environment file URL to load when the `source` is set to a YQL query.
                Set this to `null` to use the default Open Data Tables environment file
                (http://datatables.org/alltables.env).
            
                @attribute yqlEnv
                @type String
                @default null
                @for AutoCompleteBase
                **/
                yqlEnv: {
                    value: null
                },
            
                /**
                URL protocol to use when the `source` is set to a YQL query.
            
                @attribute yqlProtocol
                @type String
                @default 'http'
                @for AutoCompleteBase
                **/
                yqlProtocol: {
                    value: 'http'
                }
            });
            
            // Tell AutoCompleteBase about the new source types it can now support.
            Y.mix(ACBase.SOURCE_TYPES, {
                io    : '_createIOSource',
                jsonp : '_createJSONPSource',
                object: '_beforeCreateObjectSource', // Run our version before the base version.
                select: '_createSelectSource',
                string: '_createStringSource',
                yql   : '_createYQLSource'
            }, true);