API Docs for: 3.13.0
Show:

File: autocomplete/js/autocomplete-sources.js

  1. /**
  2. Mixes support for JSONP and YQL result sources into AutoCompleteBase.
  3.  
  4. @module autocomplete
  5. @submodule autocomplete-sources
  6. **/
  7.  
  8. var ACBase = Y.AutoCompleteBase,
  9. Lang = Y.Lang,
  10.  
  11. _SOURCE_SUCCESS = '_sourceSuccess',
  12.  
  13. MAX_RESULTS = 'maxResults',
  14. REQUEST_TEMPLATE = 'requestTemplate',
  15. RESULT_LIST_LOCATOR = 'resultListLocator';
  16.  
  17. // Add prototype properties and methods to AutoCompleteBase.
  18. Y.mix(ACBase.prototype, {
  19. /**
  20. Regular expression used to determine whether a String source is a YQL query.
  21.  
  22. @property _YQL_SOURCE_REGEX
  23. @type RegExp
  24. @protected
  25. @for AutoCompleteBase
  26. **/
  27. _YQL_SOURCE_REGEX: /^(?:select|set|use)\s+/i,
  28.  
  29. /**
  30. Runs before AutoCompleteBase's `_createObjectSource()` method and augments
  31. it to support additional object-based source types.
  32.  
  33. @method _beforeCreateObjectSource
  34. @param {String} source
  35. @protected
  36. @for AutoCompleteBase
  37. **/
  38. _beforeCreateObjectSource: function (source) {
  39. // If the object is a <select> node, use the options as the result
  40. // source.
  41. if (source instanceof Y.Node &&
  42. source.get('nodeName').toLowerCase() === 'select') {
  43.  
  44. return this._createSelectSource(source);
  45. }
  46.  
  47. // If the object is a JSONPRequest instance, try to use it as a JSONP
  48. // source.
  49. if (Y.JSONPRequest && source instanceof Y.JSONPRequest) {
  50. return this._createJSONPSource(source);
  51. }
  52.  
  53. // Fall back to a basic object source.
  54. return this._createObjectSource(source);
  55. },
  56.  
  57. /**
  58. Creates a DataSource-like object that uses `Y.io` as a source. See the
  59. `source` attribute for more details.
  60.  
  61. @method _createIOSource
  62. @param {String} source URL.
  63. @return {Object} DataSource-like object.
  64. @protected
  65. @for AutoCompleteBase
  66. **/
  67. _createIOSource: function (source) {
  68. var ioSource = {type: 'io'},
  69. that = this,
  70. ioRequest, lastRequest, loading;
  71.  
  72. // Private internal _sendRequest method that will be assigned to
  73. // ioSource.sendRequest once io-base and json-parse are available.
  74. function _sendRequest(request) {
  75. var cacheKey = request.request;
  76.  
  77. // Return immediately on a cached response.
  78. if (that._cache && cacheKey in that._cache) {
  79. that[_SOURCE_SUCCESS](that._cache[cacheKey], request);
  80. return;
  81. }
  82.  
  83. // Cancel any outstanding requests.
  84. if (ioRequest && ioRequest.isInProgress()) {
  85. ioRequest.abort();
  86. }
  87.  
  88. ioRequest = Y.io(that._getXHRUrl(source, request), {
  89. on: {
  90. success: function (tid, response) {
  91. var data;
  92.  
  93. try {
  94. data = Y.JSON.parse(response.responseText);
  95. } catch (ex) {
  96. Y.error('JSON parse error', ex);
  97. }
  98.  
  99. if (data) {
  100. that._cache && (that._cache[cacheKey] = data);
  101. that[_SOURCE_SUCCESS](data, request);
  102. }
  103. }
  104. }
  105. });
  106. }
  107.  
  108. ioSource.sendRequest = function (request) {
  109. // Keep track of the most recent request in case there are multiple
  110. // requests while we're waiting for the IO module to load. Only the
  111. // most recent request will be sent.
  112. lastRequest = request;
  113.  
  114. if (loading) { return; }
  115.  
  116. loading = true;
  117.  
  118. // Lazy-load the io-base and json-parse modules if necessary,
  119. // then overwrite the sendRequest method to bypass this check in
  120. // the future.
  121. Y.use('io-base', 'json-parse', function () {
  122. ioSource.sendRequest = _sendRequest;
  123. _sendRequest(lastRequest);
  124. });
  125. };
  126.  
  127. return ioSource;
  128. },
  129.  
  130. /**
  131. Creates a DataSource-like object that uses the specified JSONPRequest
  132. instance as a source. See the `source` attribute for more details.
  133.  
  134. @method _createJSONPSource
  135. @param {JSONPRequest|String} source URL string or JSONPRequest instance.
  136. @return {Object} DataSource-like object.
  137. @protected
  138. @for AutoCompleteBase
  139. **/
  140. _createJSONPSource: function (source) {
  141. var jsonpSource = {type: 'jsonp'},
  142. that = this,
  143. lastRequest, loading;
  144.  
  145. function _sendRequest(request) {
  146. var cacheKey = request.request,
  147. query = request.query;
  148.  
  149. if (that._cache && cacheKey in that._cache) {
  150. that[_SOURCE_SUCCESS](that._cache[cacheKey], request);
  151. return;
  152. }
  153.  
  154. // Hack alert: JSONPRequest currently doesn't support
  155. // per-request callbacks, so we're reaching into the protected
  156. // _config object to make it happen.
  157. //
  158. // This limitation is mentioned in the following JSONP
  159. // enhancement ticket:
  160. //
  161. // http://yuilibrary.com/projects/yui3/ticket/2529371
  162. source._config.on.success = function (data) {
  163. that._cache && (that._cache[cacheKey] = data);
  164. that[_SOURCE_SUCCESS](data, request);
  165. };
  166.  
  167. source.send(query);
  168. }
  169.  
  170. jsonpSource.sendRequest = function (request) {
  171. // Keep track of the most recent request in case there are multiple
  172. // requests while we're waiting for the JSONP module to load. Only
  173. // the most recent request will be sent.
  174. lastRequest = request;
  175.  
  176. if (loading) { return; }
  177.  
  178. loading = true;
  179.  
  180. // Lazy-load the JSONP module if necessary, then overwrite the
  181. // sendRequest method to bypass this check in the future.
  182. Y.use('jsonp', function () {
  183. // Turn the source into a JSONPRequest instance if it isn't
  184. // one already.
  185. if (!(source instanceof Y.JSONPRequest)) {
  186. source = new Y.JSONPRequest(source, {
  187. format: Y.bind(that._jsonpFormatter, that)
  188. });
  189. }
  190.  
  191. jsonpSource.sendRequest = _sendRequest;
  192. _sendRequest(lastRequest);
  193. });
  194. };
  195.  
  196. return jsonpSource;
  197. },
  198.  
  199. /**
  200. Creates a DataSource-like object that uses the specified `<select>` node as
  201. a source.
  202.  
  203. @method _createSelectSource
  204. @param {Node} source YUI Node instance wrapping a `<select>` node.
  205. @return {Object} DataSource-like object.
  206. @protected
  207. @for AutoCompleteBase
  208. **/
  209. _createSelectSource: function (source) {
  210. var that = this;
  211.  
  212. return {
  213. type: 'select',
  214. sendRequest: function (request) {
  215. var options = [];
  216.  
  217. source.get('options').each(function (option) {
  218. options.push({
  219. html : option.get('innerHTML'),
  220. index : option.get('index'),
  221. node : option,
  222. selected: option.get('selected'),
  223. text : option.get('text'),
  224. value : option.get('value')
  225. });
  226. });
  227.  
  228. that[_SOURCE_SUCCESS](options, request);
  229. }
  230. };
  231. },
  232.  
  233. /**
  234. Creates a DataSource-like object that calls the specified URL or executes
  235. the specified YQL query for results. If the string starts with "select ",
  236. "use ", or "set " (case-insensitive), it's assumed to be a YQL query;
  237. otherwise, it's assumed to be a URL (which may be absolute or relative).
  238. URLs containing a "{callback}" placeholder are assumed to be JSONP URLs; all
  239. others will use XHR. See the `source` attribute for more details.
  240.  
  241. @method _createStringSource
  242. @param {String} source URL or YQL query.
  243. @return {Object} DataSource-like object.
  244. @protected
  245. @for AutoCompleteBase
  246. **/
  247. _createStringSource: function (source) {
  248. if (this._YQL_SOURCE_REGEX.test(source)) {
  249. // Looks like a YQL query.
  250. return this._createYQLSource(source);
  251. } else if (source.indexOf('{callback}') !== -1) {
  252. // Contains a {callback} param and isn't a YQL query, so it must be
  253. // JSONP.
  254. return this._createJSONPSource(source);
  255. } else {
  256. // Not a YQL query or JSONP, so we'll assume it's an XHR URL.
  257. return this._createIOSource(source);
  258. }
  259. },
  260.  
  261. /**
  262. Creates a DataSource-like object that uses the specified YQL query string to
  263. create a YQL-based source. See the `source` attribute for details. If no
  264. `resultListLocator` is defined, this method will set a best-guess locator
  265. that might work for many typical YQL queries.
  266.  
  267. @method _createYQLSource
  268. @param {String} source YQL query.
  269. @return {Object} DataSource-like object.
  270. @protected
  271. @for AutoCompleteBase
  272. **/
  273. _createYQLSource: function (source) {
  274. var that = this,
  275. yqlSource = {type: 'yql'},
  276. lastRequest, loading, yqlRequest;
  277.  
  278. if (!that.get(RESULT_LIST_LOCATOR)) {
  279. that.set(RESULT_LIST_LOCATOR, that._defaultYQLLocator);
  280. }
  281.  
  282. function _sendRequest(request) {
  283. var query = request.query,
  284. env = that.get('yqlEnv'),
  285. maxResults = that.get(MAX_RESULTS),
  286. callback, opts, yqlQuery;
  287.  
  288. yqlQuery = Lang.sub(source, {
  289. maxResults: maxResults > 0 ? maxResults : 1000,
  290. request : request.request,
  291. query : query
  292. });
  293.  
  294. if (that._cache && yqlQuery in that._cache) {
  295. that[_SOURCE_SUCCESS](that._cache[yqlQuery], request);
  296. return;
  297. }
  298.  
  299. callback = function (data) {
  300. that._cache && (that._cache[yqlQuery] = data);
  301. that[_SOURCE_SUCCESS](data, request);
  302. };
  303.  
  304. opts = {proto: that.get('yqlProtocol')};
  305.  
  306. // Only create a new YQLRequest instance if this is the
  307. // first request. For subsequent requests, we'll reuse the
  308. // original instance.
  309. if (yqlRequest) {
  310. yqlRequest._callback = callback;
  311. yqlRequest._opts = opts;
  312. yqlRequest._params.q = yqlQuery;
  313.  
  314. if (env) {
  315. yqlRequest._params.env = env;
  316. }
  317. } else {
  318. yqlRequest = new Y.YQLRequest(yqlQuery, {
  319. on: {success: callback},
  320. allowCache: false // temp workaround until JSONP has per-URL callback proxies
  321. }, env ? {env: env} : null, opts);
  322. }
  323.  
  324. yqlRequest.send();
  325. }
  326.  
  327. yqlSource.sendRequest = function (request) {
  328. // Keep track of the most recent request in case there are multiple
  329. // requests while we're waiting for the YQL module to load. Only the
  330. // most recent request will be sent.
  331. lastRequest = request;
  332.  
  333. if (!loading) {
  334. // Lazy-load the YQL module if necessary, then overwrite the
  335. // sendRequest method to bypass this check in the future.
  336. loading = true;
  337.  
  338. Y.use('yql', function () {
  339. yqlSource.sendRequest = _sendRequest;
  340. _sendRequest(lastRequest);
  341. });
  342. }
  343. };
  344.  
  345. return yqlSource;
  346. },
  347.  
  348. /**
  349. Default resultListLocator used when a string-based YQL source is set and the
  350. implementer hasn't already specified one.
  351.  
  352. @method _defaultYQLLocator
  353. @param {Object} response YQL response object.
  354. @return {Array}
  355. @protected
  356. @for AutoCompleteBase
  357. **/
  358. _defaultYQLLocator: function (response) {
  359. var results = response && response.query && response.query.results,
  360. values;
  361.  
  362. if (results && Lang.isObject(results)) {
  363. // If there's only a single value on YQL's results object, that
  364. // value almost certainly contains the array of results we want. If
  365. // there are 0 or 2+ values, then the values themselves are most
  366. // likely the results we want.
  367. values = Y.Object.values(results) || [];
  368. results = values.length === 1 ? values[0] : values;
  369.  
  370. if (!Lang.isArray(results)) {
  371. results = [results];
  372. }
  373. } else {
  374. results = [];
  375. }
  376.  
  377. return results;
  378. },
  379.  
  380. /**
  381. Returns a formatted XHR URL based on the specified base _url_, _query_, and
  382. the current _requestTemplate_ if any.
  383.  
  384. @method _getXHRUrl
  385. @param {String} url Base URL.
  386. @param {Object} request Request object containing `query` and `request`
  387. properties.
  388. @return {String} Formatted URL.
  389. @protected
  390. @for AutoCompleteBase
  391. **/
  392. _getXHRUrl: function (url, request) {
  393. var maxResults = this.get(MAX_RESULTS);
  394.  
  395. if (request.query !== request.request) {
  396. // Append the request template to the URL.
  397. url += request.request;
  398. }
  399.  
  400. return Lang.sub(url, {
  401. maxResults: maxResults > 0 ? maxResults : 1000,
  402. query : encodeURIComponent(request.query)
  403. });
  404. },
  405.  
  406. /**
  407. URL formatter passed to `JSONPRequest` instances.
  408.  
  409. @method _jsonpFormatter
  410. @param {String} url
  411. @param {String} proxy
  412. @param {String} query
  413. @return {String} Formatted URL
  414. @protected
  415. @for AutoCompleteBase
  416. **/
  417. _jsonpFormatter: function (url, proxy, query) {
  418. var maxResults = this.get(MAX_RESULTS),
  419. requestTemplate = this.get(REQUEST_TEMPLATE);
  420.  
  421. if (requestTemplate) {
  422. url += requestTemplate(query);
  423. }
  424.  
  425. return Lang.sub(url, {
  426. callback : proxy,
  427. maxResults: maxResults > 0 ? maxResults : 1000,
  428. query : encodeURIComponent(query)
  429. });
  430. }
  431. });
  432.  
  433. // Add attributes to AutoCompleteBase.
  434. Y.mix(ACBase.ATTRS, {
  435. /**
  436. YQL environment file URL to load when the `source` is set to a YQL query.
  437. Set this to `null` to use the default Open Data Tables environment file
  438. (http://datatables.org/alltables.env).
  439.  
  440. @attribute yqlEnv
  441. @type String
  442. @default null
  443. @for AutoCompleteBase
  444. **/
  445. yqlEnv: {
  446. value: null
  447. },
  448.  
  449. /**
  450. URL protocol to use when the `source` is set to a YQL query.
  451.  
  452. @attribute yqlProtocol
  453. @type String
  454. @default 'http'
  455. @for AutoCompleteBase
  456. **/
  457. yqlProtocol: {
  458. value: 'http'
  459. }
  460. });
  461.  
  462. // Tell AutoCompleteBase about the new source types it can now support.
  463. Y.mix(ACBase.SOURCE_TYPES, {
  464. io : '_createIOSource',
  465. jsonp : '_createJSONPSource',
  466. object: '_beforeCreateObjectSource', // Run our version before the base version.
  467. select: '_createSelectSource',
  468. string: '_createStringSource',
  469. yql : '_createYQLSource'
  470. }, true);
  471.