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
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, query);
  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 );