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.

633 lines
20 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /*
  2. * # Semantic - API
  3. * http://github.com/jlukic/semantic-ui/
  4. *
  5. *
  6. * Copyright 2013 Contributors
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ( $, window, document, undefined ) {
  12. $.api = $.fn.api = function(parameters) {
  13. var
  14. settings = $.extend(true, {}, $.api.settings, parameters),
  15. // if this keyword isn't a jQuery object, create one
  16. context = (typeof this != 'function')
  17. ? this
  18. : $('<div/>'),
  19. // context defines the element used for loading/error state
  20. $context = (settings.stateContext)
  21. ? $(settings.stateContext)
  22. : $(context),
  23. // module is the thing that initiates the api action, can be independent of context
  24. $module = typeof this == 'object'
  25. ? $(context)
  26. : $context,
  27. element = this,
  28. time = new Date().getTime(),
  29. performance = [],
  30. moduleSelector = $module.selector || '',
  31. moduleNamespace = settings.namespace + '-module',
  32. className = settings.className,
  33. metadata = settings.metadata,
  34. error = settings.error,
  35. instance = $module.data(moduleNamespace),
  36. query = arguments[0],
  37. methodInvoked = (instance !== undefined && typeof query == 'string'),
  38. queryArguments = [].slice.call(arguments, 1),
  39. module,
  40. returnedValue
  41. ;
  42. module = {
  43. initialize: function() {
  44. var
  45. runSettings,
  46. loadingTimer = new Date().getTime(),
  47. loadingDelay,
  48. promise,
  49. url,
  50. formData = {},
  51. data,
  52. ajaxSettings = {},
  53. xhr
  54. ;
  55. // serialize parent form if requested!
  56. if(settings.serializeForm && $(this).toJSON() !== undefined) {
  57. formData = module.get.formData();
  58. module.debug('Adding form data to API Request', formData);
  59. $.extend(true, settings.data, formData);
  60. }
  61. // let beforeSend change settings object
  62. runSettings = $.proxy(settings.beforeSend, $module)(settings);
  63. // check for exit conditions
  64. if(runSettings !== undefined && !runSettings) {
  65. module.error(error.beforeSend);
  66. module.reset();
  67. return;
  68. }
  69. // get real url from template
  70. url = module.get.url( module.get.templateURL() );
  71. // exit conditions reached from missing url parameters
  72. if( !url ) {
  73. module.error(error.missingURL);
  74. module.reset();
  75. return;
  76. }
  77. // promise handles notification on api request, so loading min. delay can occur for all notifications
  78. promise =
  79. $.Deferred()
  80. .always(function() {
  81. if(settings.stateContext) {
  82. $context
  83. .removeClass(className.loading)
  84. ;
  85. }
  86. $.proxy(settings.complete, $module)();
  87. })
  88. .done(function(response) {
  89. module.debug('API request successful');
  90. // take a stab at finding success state if json
  91. if(settings.dataType == 'json') {
  92. if (response.error !== undefined) {
  93. $.proxy(settings.failure, $context)(response.error, settings, $module);
  94. }
  95. else if ($.isArray(response.errors)) {
  96. $.proxy(settings.failure, $context)(response.errors[0], settings, $module);
  97. }
  98. else {
  99. $.proxy(settings.success, $context)(response, settings, $module);
  100. }
  101. }
  102. // otherwise
  103. else {
  104. $.proxy(settings.success, $context)(response, settings, $module);
  105. }
  106. })
  107. .fail(function(xhr, status, httpMessage) {
  108. var
  109. errorMessage = (settings.error[status] !== undefined)
  110. ? settings.error[status]
  111. : httpMessage,
  112. response
  113. ;
  114. // let em know unless request aborted
  115. if(xhr !== undefined) {
  116. // readyState 4 = done, anything less is not really sent
  117. if(xhr.readyState !== undefined && xhr.readyState == 4) {
  118. // if http status code returned and json returned error, look for it
  119. if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
  120. module.error(error.statusMessage + httpMessage);
  121. }
  122. else {
  123. if(status == 'error' && settings.dataType == 'json') {
  124. try {
  125. response = $.parseJSON(xhr.responseText);
  126. if(response && response.error !== undefined) {
  127. errorMessage = response.error;
  128. }
  129. }
  130. catch(error) {
  131. module.error(error.JSONParse);
  132. }
  133. }
  134. }
  135. $context
  136. .removeClass(className.loading)
  137. .addClass(className.error)
  138. ;
  139. // show error state only for duration specified in settings
  140. if(settings.errorLength > 0) {
  141. setTimeout(function(){
  142. $context
  143. .removeClass(className.error)
  144. ;
  145. }, settings.errorLength);
  146. }
  147. module.debug('API Request error:', errorMessage);
  148. $.proxy(settings.failure, $context)(errorMessage, settings, this);
  149. }
  150. else {
  151. module.debug('Request Aborted (Most likely caused by page change)');
  152. }
  153. }
  154. })
  155. ;
  156. // look for params in data
  157. $.extend(true, ajaxSettings, settings, {
  158. success : function(){},
  159. failure : function(){},
  160. complete : function(){},
  161. type : settings.method || settings.type,
  162. data : data,
  163. url : url,
  164. beforeSend : settings.beforeXHR
  165. });
  166. if(settings.stateContext) {
  167. $context
  168. .addClass(className.loading)
  169. ;
  170. }
  171. if(settings.progress) {
  172. module.verbose('Adding progress events');
  173. $.extend(true, ajaxSettings, {
  174. xhr: function() {
  175. var
  176. xhr = new window.XMLHttpRequest()
  177. ;
  178. xhr.upload.addEventListener('progress', function(event) {
  179. var
  180. percentComplete
  181. ;
  182. if (event.lengthComputable) {
  183. percentComplete = Math.round(event.loaded / event.total * 10000) / 100 + '%';
  184. $.proxy(settings.progress, $context)(percentComplete, event);
  185. }
  186. }, false);
  187. xhr.addEventListener('progress', function(event) {
  188. var
  189. percentComplete
  190. ;
  191. if (event.lengthComputable) {
  192. percentComplete = Math.round(event.loaded / event.total * 10000) / 100 + '%';
  193. $.proxy(settings.progress, $context)(percentComplete, event);
  194. }
  195. }, false);
  196. return xhr;
  197. }
  198. });
  199. }
  200. module.verbose('Creating AJAX request with settings: ', ajaxSettings);
  201. xhr =
  202. $.ajax(ajaxSettings)
  203. .always(function() {
  204. // calculate if loading time was below minimum threshold
  205. loadingDelay = ( settings.loadingLength - (new Date().getTime() - loadingTimer) );
  206. settings.loadingDelay = loadingDelay < 0
  207. ? 0
  208. : loadingDelay
  209. ;
  210. })
  211. .done(function(response) {
  212. var
  213. context = this
  214. ;
  215. setTimeout(function(){
  216. promise.resolveWith(context, [response]);
  217. }, settings.loadingDelay);
  218. })
  219. .fail(function(xhr, status, httpMessage) {
  220. var
  221. context = this
  222. ;
  223. // page triggers abort on navigation, dont show error
  224. if(status != 'abort') {
  225. setTimeout(function(){
  226. promise.rejectWith(context, [xhr, status, httpMessage]);
  227. }, settings.loadingDelay);
  228. }
  229. else {
  230. $context
  231. .removeClass(className.error)
  232. .removeClass(className.loading)
  233. ;
  234. }
  235. })
  236. ;
  237. if(settings.stateContext) {
  238. $module
  239. .data(metadata.promise, promise)
  240. .data(metadata.xhr, xhr)
  241. ;
  242. }
  243. },
  244. get: {
  245. formData: function() {
  246. return $module
  247. .closest('form')
  248. .toJSON()
  249. ;
  250. },
  251. templateURL: function() {
  252. var
  253. action = $module.data(settings.metadata.action) || settings.action || false,
  254. url
  255. ;
  256. if(action) {
  257. module.debug('Creating url for: ', action);
  258. if(settings.api[action] !== undefined) {
  259. url = settings.api[action];
  260. }
  261. else {
  262. module.error(error.missingAction);
  263. }
  264. }
  265. // override with url if specified
  266. if(settings.url) {
  267. url = settings.url;
  268. module.debug('Getting url', url);
  269. }
  270. return url;
  271. },
  272. url: function(url, urlData) {
  273. var
  274. urlVariables
  275. ;
  276. if(url) {
  277. urlVariables = url.match(settings.regExpTemplate);
  278. urlData = urlData || settings.urlData;
  279. if(urlVariables) {
  280. module.debug('Looking for URL variables', urlVariables);
  281. $.each(urlVariables, function(index, templateValue){
  282. var
  283. term = templateValue.substr( 2, templateValue.length - 3),
  284. termValue = ($.isPlainObject(urlData) && urlData[term] !== undefined)
  285. ? urlData[term]
  286. : ($module.data(term) !== undefined)
  287. ? $module.data(term)
  288. : urlData[term]
  289. ;
  290. module.verbose('Looking for variable', term, $module, $module.data(term), urlData[term]);
  291. // remove optional value
  292. if(termValue === false) {
  293. module.debug('Removing variable from URL', urlVariables);
  294. url = url.replace('/' + templateValue, '');
  295. }
  296. // undefined condition
  297. else if(termValue === undefined || !termValue) {
  298. module.error(error.missingParameter + term);
  299. url = false;
  300. return false;
  301. }
  302. else {
  303. url = url.replace(templateValue, termValue);
  304. }
  305. });
  306. }
  307. }
  308. return url;
  309. }
  310. },
  311. // reset api request
  312. reset: function() {
  313. $module
  314. .data(metadata.promise, false)
  315. .data(metadata.xhr, false)
  316. ;
  317. $context
  318. .removeClass(className.error)
  319. .removeClass(className.loading)
  320. ;
  321. },
  322. setting: function(name, value) {
  323. if( $.isPlainObject(name) ) {
  324. $.extend(true, settings, name);
  325. }
  326. else if(value !== undefined) {
  327. settings[name] = value;
  328. }
  329. else {
  330. return settings[name];
  331. }
  332. },
  333. internal: function(name, value) {
  334. if( $.isPlainObject(name) ) {
  335. $.extend(true, module, name);
  336. }
  337. else if(value !== undefined) {
  338. module[name] = value;
  339. }
  340. else {
  341. return module[name];
  342. }
  343. },
  344. debug: function() {
  345. if(settings.debug) {
  346. if(settings.performance) {
  347. module.performance.log(arguments);
  348. }
  349. else {
  350. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  351. module.debug.apply(console, arguments);
  352. }
  353. }
  354. },
  355. verbose: function() {
  356. if(settings.verbose && settings.debug) {
  357. if(settings.performance) {
  358. module.performance.log(arguments);
  359. }
  360. else {
  361. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  362. module.verbose.apply(console, arguments);
  363. }
  364. }
  365. },
  366. error: function() {
  367. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  368. module.error.apply(console, arguments);
  369. },
  370. performance: {
  371. log: function(message) {
  372. var
  373. currentTime,
  374. executionTime,
  375. previousTime
  376. ;
  377. if(settings.performance) {
  378. currentTime = new Date().getTime();
  379. previousTime = time || currentTime;
  380. executionTime = currentTime - previousTime;
  381. time = currentTime;
  382. performance.push({
  383. 'Element' : element,
  384. 'Name' : message[0],
  385. 'Arguments' : [].slice.call(message, 1) || '',
  386. 'Execution Time' : executionTime
  387. });
  388. }
  389. clearTimeout(module.performance.timer);
  390. module.performance.timer = setTimeout(module.performance.display, 100);
  391. },
  392. display: function() {
  393. var
  394. title = settings.name + ':',
  395. totalTime = 0
  396. ;
  397. time = false;
  398. clearTimeout(module.performance.timer);
  399. $.each(performance, function(index, data) {
  400. totalTime += data['Execution Time'];
  401. });
  402. title += ' ' + totalTime + 'ms';
  403. if(moduleSelector) {
  404. title += ' \'' + moduleSelector + '\'';
  405. }
  406. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  407. console.groupCollapsed(title);
  408. if(console.table) {
  409. console.table(performance);
  410. }
  411. else {
  412. $.each(performance, function(index, data) {
  413. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  414. });
  415. }
  416. console.groupEnd();
  417. }
  418. performance = [];
  419. }
  420. },
  421. invoke: function(query, passedArguments, context) {
  422. var
  423. maxDepth,
  424. found,
  425. response
  426. ;
  427. passedArguments = passedArguments || queryArguments;
  428. context = element || context;
  429. if(typeof query == 'string' && instance !== undefined) {
  430. query = query.split(/[\. ]/);
  431. maxDepth = query.length - 1;
  432. $.each(query, function(depth, value) {
  433. var camelCaseValue = (depth != maxDepth)
  434. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  435. : query
  436. ;
  437. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  438. instance = instance[value];
  439. }
  440. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  441. instance = instance[camelCaseValue];
  442. }
  443. else if( instance[value] !== undefined ) {
  444. found = instance[value];
  445. return false;
  446. }
  447. else if( instance[camelCaseValue] !== undefined ) {
  448. found = instance[camelCaseValue];
  449. return false;
  450. }
  451. else {
  452. module.error(error.method);
  453. return false;
  454. }
  455. });
  456. }
  457. if ( $.isFunction( found ) ) {
  458. response = found.apply(context, passedArguments);
  459. }
  460. else if(found !== undefined) {
  461. response = found;
  462. }
  463. if($.isArray(returnedValue)) {
  464. returnedValue.push(response);
  465. }
  466. else if(returnedValue !== undefined) {
  467. returnedValue = [returnedValue, response];
  468. }
  469. else if(response !== undefined) {
  470. returnedValue = response;
  471. }
  472. return found;
  473. }
  474. };
  475. if(methodInvoked) {
  476. if(instance === undefined) {
  477. module.initialize();
  478. }
  479. module.invoke(query);
  480. }
  481. else {
  482. if(instance !== undefined) {
  483. module.destroy();
  484. }
  485. module.initialize();
  486. }
  487. return (returnedValue !== undefined)
  488. ? returnedValue
  489. : this
  490. ;
  491. };
  492. // handle DOM attachment to API functionality
  493. $.fn.apiButton = function(parameters) {
  494. $(this)
  495. .each(function(){
  496. var
  497. // if only function passed it is success callback
  498. $module = $(this),
  499. selector = $(this).selector || '',
  500. settings = ( $.isFunction(parameters) )
  501. ? $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this, success: parameters })
  502. : $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this}, parameters),
  503. module
  504. ;
  505. module = {
  506. initialize: function() {
  507. if(settings.context && selector !== '') {
  508. $(settings.context)
  509. .on(selector, 'click.' + settings.namespace, module.click)
  510. ;
  511. }
  512. else {
  513. $module
  514. .on('click.' + settings.namespace, module.click)
  515. ;
  516. }
  517. },
  518. click: function() {
  519. if(!settings.filter || $(this).filter(settings.filter).size() === 0) {
  520. $.proxy( $.api, this )(settings);
  521. }
  522. }
  523. };
  524. module.initialize();
  525. })
  526. ;
  527. return this;
  528. };
  529. $.api.settings = {
  530. name : 'API',
  531. namespace : 'api',
  532. debug : true,
  533. verbose : true,
  534. performance : true,
  535. api : {},
  536. beforeSend : function(settings) {
  537. return settings;
  538. },
  539. beforeXHR : function(xhr) {},
  540. success : function(response) {},
  541. complete : function(response) {},
  542. failure : function(errorCode) {},
  543. progress : false,
  544. error : {
  545. missingAction : 'API action used but no url was defined',
  546. missingURL : 'URL not specified for the API action',
  547. missingParameter : 'Missing an essential URL parameter: ',
  548. timeout : 'Your request timed out',
  549. error : 'There was an error with your request',
  550. parseError : 'There was an error parsing your request',
  551. JSONParse : 'JSON could not be parsed during error handling',
  552. statusMessage : 'Server gave an error: ',
  553. beforeSend : 'The before send function has aborted the request',
  554. exitConditions : 'API Request Aborted. Exit conditions met'
  555. },
  556. className: {
  557. loading : 'loading',
  558. error : 'error'
  559. },
  560. metadata: {
  561. action : 'action',
  562. promise : 'promise',
  563. xhr : 'xhr'
  564. },
  565. regExpTemplate: /\{\$([A-z]+)\}/g,
  566. action : false,
  567. url : false,
  568. urlData : false,
  569. serializeForm : false,
  570. stateContext : false,
  571. method : 'get',
  572. data : {},
  573. dataType : 'json',
  574. cache : true,
  575. loadingLength : 200,
  576. errorLength : 2000
  577. };
  578. $.fn.apiButton.settings = {
  579. filter : '.disabled, .loading',
  580. context : false,
  581. stateContext : false
  582. };
  583. })( jQuery, window , document );