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.

637 lines
20 KiB

  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. invokedResponse
  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(value !== undefined) {
  324. if( $.isPlainObject(name) ) {
  325. $.extend(true, settings, name);
  326. }
  327. else {
  328. settings[name] = value;
  329. }
  330. }
  331. else {
  332. return settings[name];
  333. }
  334. },
  335. internal: function(name, value) {
  336. if(value !== undefined) {
  337. if( $.isPlainObject(name) ) {
  338. $.extend(true, module, name);
  339. }
  340. else {
  341. module[name] = value;
  342. }
  343. }
  344. else {
  345. return module[name];
  346. }
  347. },
  348. debug: function() {
  349. if(settings.debug) {
  350. if(settings.performance) {
  351. module.performance.log(arguments);
  352. }
  353. else {
  354. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  355. module.debug.apply(console, arguments);
  356. }
  357. }
  358. },
  359. verbose: function() {
  360. if(settings.verbose && settings.debug) {
  361. if(settings.performance) {
  362. module.performance.log(arguments);
  363. }
  364. else {
  365. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  366. module.verbose.apply(console, arguments);
  367. }
  368. }
  369. },
  370. error: function() {
  371. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  372. module.error.apply(console, arguments);
  373. },
  374. performance: {
  375. log: function(message) {
  376. var
  377. currentTime,
  378. executionTime,
  379. previousTime
  380. ;
  381. if(settings.performance) {
  382. currentTime = new Date().getTime();
  383. previousTime = time || currentTime;
  384. executionTime = currentTime - previousTime;
  385. time = currentTime;
  386. performance.push({
  387. 'Element' : element,
  388. 'Name' : message[0],
  389. 'Arguments' : [].slice.call(message, 1) || '',
  390. 'Execution Time' : executionTime
  391. });
  392. }
  393. clearTimeout(module.performance.timer);
  394. module.performance.timer = setTimeout(module.performance.display, 100);
  395. },
  396. display: function() {
  397. var
  398. title = settings.name + ':',
  399. totalTime = 0
  400. ;
  401. time = false;
  402. clearTimeout(module.performance.timer);
  403. $.each(performance, function(index, data) {
  404. totalTime += data['Execution Time'];
  405. });
  406. title += ' ' + totalTime + 'ms';
  407. if(moduleSelector) {
  408. title += ' \'' + moduleSelector + '\'';
  409. }
  410. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  411. console.groupCollapsed(title);
  412. if(console.table) {
  413. console.table(performance);
  414. }
  415. else {
  416. $.each(performance, function(index, data) {
  417. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  418. });
  419. }
  420. console.groupEnd();
  421. }
  422. performance = [];
  423. }
  424. },
  425. invoke: function(query, passedArguments, context) {
  426. var
  427. maxDepth,
  428. found,
  429. response
  430. ;
  431. passedArguments = passedArguments || queryArguments;
  432. context = element || context;
  433. if(typeof query == 'string' && instance !== undefined) {
  434. query = query.split(/[\. ]/);
  435. maxDepth = query.length - 1;
  436. $.each(query, function(depth, value) {
  437. var camelCaseValue = (depth != maxDepth)
  438. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  439. : query
  440. ;
  441. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  442. instance = instance[value];
  443. }
  444. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  445. instance = instance[camelCaseValue];
  446. }
  447. else if( instance[value] !== undefined ) {
  448. found = instance[value];
  449. return false;
  450. }
  451. else if( instance[camelCaseValue] !== undefined ) {
  452. found = instance[camelCaseValue];
  453. return false;
  454. }
  455. else {
  456. module.error(error.method);
  457. return false;
  458. }
  459. });
  460. }
  461. if ( $.isFunction( found ) ) {
  462. response = found.apply(context, passedArguments);
  463. }
  464. else if(found !== undefined) {
  465. response = found;
  466. }
  467. if($.isArray(invokedResponse)) {
  468. invokedResponse.push(response);
  469. }
  470. else if(typeof invokedResponse == 'string') {
  471. invokedResponse = [invokedResponse, response];
  472. }
  473. else if(response !== undefined) {
  474. invokedResponse = response;
  475. }
  476. return found;
  477. }
  478. };
  479. if(methodInvoked) {
  480. if(instance === undefined) {
  481. module.initialize();
  482. }
  483. module.invoke(query);
  484. }
  485. else {
  486. if(instance !== undefined) {
  487. module.destroy();
  488. }
  489. module.initialize();
  490. }
  491. return (invokedResponse !== undefined)
  492. ? invokedResponse
  493. : this
  494. ;
  495. };
  496. // handle DOM attachment to API functionality
  497. $.fn.apiButton = function(parameters) {
  498. $(this)
  499. .each(function(){
  500. var
  501. // if only function passed it is success callback
  502. $module = $(this),
  503. selector = $(this).selector || '',
  504. settings = ( $.isFunction(parameters) )
  505. ? $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this, success: parameters })
  506. : $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this}, parameters),
  507. module
  508. ;
  509. module = {
  510. initialize: function() {
  511. if(settings.context && selector !== '') {
  512. $(settings.context)
  513. .on(selector, 'click.' + settings.namespace, module.click)
  514. ;
  515. }
  516. else {
  517. $module
  518. .on('click.' + settings.namespace, module.click)
  519. ;
  520. }
  521. },
  522. click: function() {
  523. if(!settings.filter || $(this).filter(settings.filter).size() === 0) {
  524. $.proxy( $.api, this )(settings);
  525. }
  526. }
  527. };
  528. module.initialize();
  529. })
  530. ;
  531. return this;
  532. };
  533. $.api.settings = {
  534. name : 'API',
  535. namespace : 'api',
  536. debug : true,
  537. verbose : true,
  538. performance : true,
  539. api : {},
  540. beforeSend : function(settings) {
  541. return settings;
  542. },
  543. beforeXHR : function(xhr) {},
  544. success : function(response) {},
  545. complete : function(response) {},
  546. failure : function(errorCode) {},
  547. progress : false,
  548. error : {
  549. missingAction : 'API action used but no url was defined',
  550. missingURL : 'URL not specified for the API action',
  551. missingParameter : 'Missing an essential URL parameter: ',
  552. timeout : 'Your request timed out',
  553. error : 'There was an error with your request',
  554. parseError : 'There was an error parsing your request',
  555. JSONParse : 'JSON could not be parsed during error handling',
  556. statusMessage : 'Server gave an error: ',
  557. beforeSend : 'The before send function has aborted the request',
  558. exitConditions : 'API Request Aborted. Exit conditions met'
  559. },
  560. className: {
  561. loading : 'loading',
  562. error : 'error'
  563. },
  564. metadata: {
  565. action : 'action',
  566. promise : 'promise',
  567. xhr : 'xhr'
  568. },
  569. regExpTemplate: /\{\$([A-z]+)\}/g,
  570. action : false,
  571. url : false,
  572. urlData : false,
  573. serializeForm : false,
  574. stateContext : false,
  575. method : 'get',
  576. data : {},
  577. dataType : 'json',
  578. cache : true,
  579. loadingLength : 200,
  580. errorLength : 2000
  581. };
  582. $.fn.apiButton.settings = {
  583. filter : '.disabled, .loading',
  584. context : false,
  585. stateContext : false
  586. };
  587. })( jQuery, window , document );