Version 3.18.1
Show:

File: autocomplete/js/autocomplete-sources.js

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