You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

508 lines
16 KiB

  1. /* ******************************
  2. API
  3. Author: Jack Lukic
  4. Notes: First Commit May 08, 2012
  5. These are modules which bind API functionality to the DOM
  6. Requires: nada
  7. Initialization:
  8. $('.button')
  9. .apiButton({
  10. success: function() {}
  11. })
  12. ;
  13. in our example api is automapped to an object literal
  14. @ quirky.config.endpoint.api
  15. HTML:
  16. <div class="button" action="follow" data-id="5">
  17. URL : quirky.config.endpoint.api.follow
  18. Given Value: /follow/{$id}/
  19. Sent Value : /follow/5/
  20. (4 ways to map api endpoint, each will be looked for in succession)
  21. url mapping order:
  22. first : defined in plugin init as url (arbitrary url)
  23. second : defined in plugin init as action (action in obj literal grouping 'api')
  24. third : defined in data-url
  25. fourth : defined in data-action
  26. beforeSend:
  27. this callback can be used to modify request settings before XHR
  28. it also can be used to look for for pre-conditions to prevent API
  29. call by returning "false"
  30. ****************************** */
  31. ;(function ( $, window, document, undefined ) {
  32. $.api = $.fn.api = function(parameters) {
  33. var
  34. settings = $.extend(true, {}, $.api.settings, parameters),
  35. // if this keyword isn't a jQuery object, create one
  36. context = (typeof this != 'function')
  37. ? this
  38. : $('<div/>'),
  39. // context defines the element used for loading/error state
  40. $context = (settings.stateContext)
  41. ? $(settings.stateContext)
  42. : $(context),
  43. // module is the thing that initiates the api action, can be independent of context
  44. $module = typeof this == 'object'
  45. ? $(context)
  46. : $context,
  47. action = $module.data(settings.metadata.action) || settings.action || false,
  48. className = settings.className,
  49. metadata = settings.metadata,
  50. errors = settings.errors,
  51. module
  52. ;
  53. module = {
  54. initialize: function() {
  55. var
  56. exitConditions = false,
  57. runSettings,
  58. loadingTimer = new Date().getTime(),
  59. loadingDelay,
  60. promise,
  61. url,
  62. urlVariables,
  63. formData = {},
  64. data,
  65. ajaxSettings = {},
  66. xhr,
  67. errors = settings.errors
  68. ;
  69. // serialize parent form if requested!
  70. if(settings.serializeForm && $(this).toJSON() !== undefined) {
  71. formData = $module
  72. .closest('form')
  73. .toJSON()
  74. ;
  75. $.extend(true, settings.data, formData);
  76. module.debug('Adding form data to API Request', formData);
  77. }
  78. // let beforesend change settings object
  79. runSettings = $.proxy(settings.beforeSend, $module)(settings);
  80. // check for exit conditions
  81. if(runSettings !== undefined && !runSettings) {
  82. module.error(errors.beforeSend);
  83. module.reset();
  84. return;
  85. }
  86. if(action) {
  87. module.debug('Initializing API Request for: ', action);
  88. if(settings.api[action] !== undefined) {
  89. url = settings.api[action];
  90. }
  91. else {
  92. module.error(errors.missingAction);
  93. }
  94. }
  95. // override with url if specified
  96. if(settings.url) {
  97. url = settings.url;
  98. module.debug('Using specified url: ', url);
  99. }
  100. if(!url) {
  101. module.error(errors.missingURL);
  102. module.reset();
  103. }
  104. // replace url data in url
  105. urlVariables = url.match(settings.regExpTemplate);
  106. if(urlVariables) {
  107. module.debug('Looking for URL variables', urlVariables);
  108. $.each(urlVariables, function(index, templateValue){
  109. var
  110. term = templateValue.substr( 2, templateValue.length - 3),
  111. termValue = ($.isPlainObject(parameters.urlData) && parameters.urlData[term] !== undefined)
  112. ? parameters.urlData[term]
  113. : ($module.data(term) !== undefined)
  114. ? $module.data(term)
  115. : settings.urlData[term]
  116. ;
  117. module.verbose('Looking for variable', term, $module, $module.data(term), settings.urlData[term]);
  118. // remove optional value
  119. if(termValue === false) {
  120. module.debug('Removing variable from URL', urlVariables);
  121. url = url.replace('/' + templateValue, '');
  122. }
  123. // undefined condition
  124. else if(termValue === undefined || !termValue) {
  125. module.error(errors.missingParameter + term);
  126. exitConditions = true;
  127. }
  128. else {
  129. url = url.replace(templateValue, termValue);
  130. }
  131. });
  132. }
  133. // exit conditions reached from missing url parameters
  134. if( exitConditions ) {
  135. module.reset();
  136. return;
  137. }
  138. // promise handles notification on api request, so loading min. delay can occur for all notifications
  139. promise =
  140. $.Deferred()
  141. .always(function() {
  142. if(settings.stateContext) {
  143. $context
  144. .removeClass(className.loading)
  145. ;
  146. }
  147. $.proxy(settings.complete, $module)();
  148. })
  149. .done(function(response) {
  150. module.debug('API request successful');
  151. // take a stab at finding success state if json
  152. if(settings.dataType == 'json') {
  153. if (response.error !== undefined) {
  154. $.proxy(settings.failure, $context)(response.error, settings, $module);
  155. }
  156. else if ($.isArray(response.errors)) {
  157. $.proxy(settings.failure, $context)(response.errors[0], settings, $module);
  158. }
  159. else {
  160. $.proxy(settings.success, $context)(response, settings, $module);
  161. }
  162. }
  163. // otherwise
  164. else {
  165. $.proxy(settings.success, $context)(response, settings, $module);
  166. }
  167. })
  168. .fail(function(xhr, status, httpMessage) {
  169. var
  170. errorMessage = (settings.errors[status] !== undefined)
  171. ? settings.errors[status]
  172. : httpMessage,
  173. response
  174. ;
  175. // let em know unless request aborted
  176. if(xhr !== undefined) {
  177. // readyState 4 = done, anything less is not really sent
  178. if(xhr.readyState !== undefined && xhr.readyState == 4) {
  179. // if http status code returned and json returned error, look for it
  180. if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
  181. module.error(errors.statusMessage + httpMessage);
  182. }
  183. else {
  184. if(status == 'error' && settings.dataType == 'json') {
  185. try {
  186. response = $.parseJSON(xhr.responseText);
  187. if(response && response.error !== undefined) {
  188. errorMessage = response.error;
  189. }
  190. }
  191. catch(error) {
  192. module.error(errors.JSONParse);
  193. }
  194. }
  195. }
  196. $context
  197. .removeClass(className.loading)
  198. .addClass(className.error)
  199. ;
  200. // show error state only for duration specified in settings
  201. if(settings.errorLength > 0) {
  202. setTimeout(function(){
  203. $context
  204. .removeClass(className.error)
  205. ;
  206. }, settings.errorLength);
  207. }
  208. module.debug('API Request error:', errorMessage);
  209. $.proxy(settings.failure, $context)(errorMessage, settings, this);
  210. }
  211. else {
  212. module.debug('Request Aborted (Most likely caused by page change)');
  213. }
  214. }
  215. })
  216. ;
  217. // look for params in data
  218. $.extend(true, ajaxSettings, settings, {
  219. success : function(){},
  220. failure : function(){},
  221. complete : function(){},
  222. type : settings.method || settings.type,
  223. data : data,
  224. url : url,
  225. beforeSend : settings.beforeXHR
  226. });
  227. if(settings.stateContext) {
  228. $context
  229. .addClass(className.loading)
  230. ;
  231. }
  232. if(settings.progress) {
  233. module.verbose('Adding progress events');
  234. $.extend(true, ajaxSettings, {
  235. xhr: function() {
  236. var
  237. xhr = new window.XMLHttpRequest()
  238. ;
  239. xhr.upload.addEventListener('progress', function(event) {
  240. var
  241. percentComplete
  242. ;
  243. if (event.lengthComputable) {
  244. percentComplete = Math.round(event.loaded / event.total * 10000) / 100 + '%';
  245. $.proxy(settings.progress, $context)(percentComplete, event);
  246. }
  247. }, false);
  248. xhr.addEventListener('progress', function(event) {
  249. var
  250. percentComplete
  251. ;
  252. if (event.lengthComputable) {
  253. percentComplete = Math.round(event.loaded / event.total * 10000) / 100 + '%';
  254. $.proxy(settings.progress, $context)(percentComplete, event);
  255. }
  256. }, false);
  257. return xhr;
  258. }
  259. });
  260. }
  261. module.verbose('Creating AJAX request with settings: ', ajaxSettings);
  262. xhr =
  263. $.ajax(ajaxSettings)
  264. .always(function() {
  265. // calculate if loading time was below minimum threshold
  266. loadingDelay = ( settings.loadingLength - (new Date().getTime() - loadingTimer) );
  267. settings.loadingDelay = loadingDelay < 0
  268. ? 0
  269. : loadingDelay
  270. ;
  271. })
  272. .done(function(response) {
  273. var
  274. context = this
  275. ;
  276. setTimeout(function(){
  277. promise.resolveWith(context, [response]);
  278. }, settings.loadingDelay);
  279. })
  280. .fail(function(xhr, status, httpMessage) {
  281. var
  282. context = this
  283. ;
  284. // page triggers abort on navigation, dont show error
  285. if(status != 'abort') {
  286. setTimeout(function(){
  287. promise.rejectWith(context, [xhr, status, httpMessage]);
  288. }, settings.loadingDelay);
  289. }
  290. else {
  291. $context
  292. .removeClass(className.error)
  293. .removeClass(className.loading)
  294. ;
  295. }
  296. })
  297. ;
  298. if(settings.stateContext) {
  299. $module
  300. .data(metadata.promise, promise)
  301. .data(metadata.xhr, xhr)
  302. ;
  303. }
  304. },
  305. // reset api request
  306. reset: function() {
  307. $module
  308. .data(metadata.promise, false)
  309. .data(metadata.xhr, false)
  310. ;
  311. $context
  312. .removeClass(className.error)
  313. .removeClass(className.loading)
  314. ;
  315. module.error(errors.exitConditions);
  316. },
  317. /* standard module */
  318. setting: function(name, value) {
  319. if(value === undefined) {
  320. return settings[name];
  321. }
  322. settings[name] = value;
  323. },
  324. verbose: function() {
  325. if(settings.verbose) {
  326. module.debug.apply(this, arguments);
  327. }
  328. },
  329. debug: function() {
  330. var
  331. output = [],
  332. message = settings.moduleName + ': ' + arguments[0],
  333. variables = [].slice.call( arguments, 1 ),
  334. log = console.info || console.log || function(){}
  335. ;
  336. log = Function.prototype.bind.call(log, console);
  337. if(settings.debug) {
  338. output.push(message);
  339. log.apply(console, output.concat(variables) );
  340. }
  341. },
  342. error: function() {
  343. var
  344. output = [],
  345. errorMessage = settings.moduleName + ': ' + arguments[0],
  346. variables = [].slice.call( arguments, 1 ),
  347. log = console.warn || console.log || function(){}
  348. ;
  349. log = Function.prototype.bind.call(log, console);
  350. if(settings.debug) {
  351. output.push(errorMessage);
  352. output.concat(variables);
  353. log.apply(console, output.concat(variables) );
  354. }
  355. }
  356. };
  357. module.initialize();
  358. return this;
  359. };
  360. // handle DOM attachment to API functionality
  361. $.fn.apiButton = function(parameters) {
  362. $(this)
  363. .each(function(){
  364. var
  365. // if only function passed it is success callback
  366. $module = $(this),
  367. element = this,
  368. selector = $(this).selector || '',
  369. settings = ( $.isFunction(parameters) )
  370. ? $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this, success: parameters })
  371. : $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this}, parameters),
  372. module
  373. ;
  374. module = {
  375. initialize: function() {
  376. if(settings.context && selector !== '') {
  377. $(settings.context)
  378. .on(selector, 'click.' + settings.namespace, module.click)
  379. ;
  380. }
  381. else {
  382. $module
  383. .on('click.' + settings.namespace, module.click)
  384. ;
  385. }
  386. },
  387. click: function() {
  388. if(!settings.filter || $(this).filter(settings.filter).size() === 0) {
  389. $.proxy( $.api, this )(settings);
  390. }
  391. }
  392. };
  393. module.initialize();
  394. })
  395. ;
  396. return this;
  397. };
  398. $.api.settings = {
  399. moduleName : 'API Module',
  400. namespace : 'api',
  401. verbose : true,
  402. debug : true,
  403. api : {},
  404. beforeSend : function(settings) {
  405. return settings;
  406. },
  407. beforeXHR : function(xhr) {},
  408. success : function(response) {},
  409. complete : function(response) {},
  410. failure : function(errorCode) {},
  411. progress : false,
  412. errors : {
  413. missingAction : 'API action used but no url was defined',
  414. missingURL : 'URL not specified for the API action',
  415. missingParameter : 'Missing an essential URL parameter: ',
  416. timeout : 'Your request timed out',
  417. error : 'There was an error with your request',
  418. parseError : 'There was an error parsing your request',
  419. JSONParse : 'JSON could not be parsed during error handling',
  420. statusMessage : 'Server gave an error: ',
  421. beforeSend : 'The before send function has aborted the request',
  422. exitConditions : 'API Request Aborted. Exit conditions met'
  423. },
  424. className: {
  425. loading : 'loading',
  426. error : 'error'
  427. },
  428. metadata: {
  429. action : 'action',
  430. promise : 'promise',
  431. xhr : 'xhr'
  432. },
  433. regExpTemplate: /\{\$([A-z]+)\}/g,
  434. action : false,
  435. url : false,
  436. urlData : false,
  437. serializeForm : false,
  438. stateContext : false,
  439. method : 'get',
  440. data : {},
  441. dataType : 'json',
  442. cache : true,
  443. loadingLength : 200,
  444. errorLength : 2000
  445. };
  446. $.fn.apiButton.settings = {
  447. filter : '.disabled, .loading',
  448. context : false,
  449. stateContext : false
  450. };
  451. })( jQuery, window , document );