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.

712 lines
22 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. // use window context if none specified
  15. $allModules = $.isFunction(this)
  16. ? $(window)
  17. : $(this),
  18. moduleSelector = $allModules.selector || '',
  19. time = new Date().getTime(),
  20. performance = [],
  21. query = arguments[0],
  22. methodInvoked = (typeof query == 'string'),
  23. queryArguments = [].slice.call(arguments, 1),
  24. returnedValue
  25. ;
  26. $allModules
  27. .each(function() {
  28. var
  29. settings = $.extend(true, {}, $.fn.api.settings, parameters),
  30. // internal aliases
  31. namespace = settings.namespace,
  32. metadata = settings.metadata,
  33. selector = settings.selector,
  34. error = settings.error,
  35. className = settings.className,
  36. // define namespaces for modules
  37. eventNamespace = '.' + namespace,
  38. moduleNamespace = 'module-' + namespace,
  39. // element that creates request
  40. $module = $(this),
  41. $form = $module.closest(selector.form),
  42. // context used for state
  43. $context = (settings.stateContext)
  44. ? $(settings.stateContext)
  45. : $module,
  46. // request details
  47. ajaxSettings,
  48. requestSettings,
  49. url,
  50. data,
  51. // standard module
  52. element = this,
  53. instance = $module.data(moduleNamespace),
  54. module
  55. ;
  56. module = {
  57. initialize: function() {
  58. var
  59. triggerEvent = module.get.event()
  60. ;
  61. if(!methodInvoked) {
  62. if( triggerEvent ) {
  63. module.debug('Attaching API events to element', triggerEvent);
  64. $module
  65. .on(triggerEvent + eventNamespace, module.query)
  66. ;
  67. }
  68. else {
  69. module.query();
  70. }
  71. }
  72. module.instantiate();
  73. },
  74. instantiate: function() {
  75. module.verbose('Storing instance of module', module);
  76. instance = module;
  77. $module
  78. .data(moduleNamespace, instance)
  79. ;
  80. },
  81. destroy: function() {
  82. module.verbose('Destroying previous module for', element);
  83. $module
  84. .removeData(moduleNamespace)
  85. .off(eventNamespace)
  86. ;
  87. },
  88. query: function() {
  89. // determine if an api event already occurred
  90. if(module.is.loading() && !settings.allowMultiple) {
  91. module.debug('Request cancelled previous request is still pending');
  92. return;
  93. }
  94. // pass element metadata to url (value, text)
  95. if(settings.defaultData) {
  96. $.extend(true, settings.urlData, module.get.defaultData());
  97. }
  98. // Add form content
  99. if(settings.serializeForm) {
  100. $.extend(true, settings.data, module.get.formData());
  101. }
  102. // call beforesend and get any settings changes
  103. requestSettings = module.get.settings();
  104. // check if beforesend cancelled request
  105. if(requestSettings === false) {
  106. module.error(error.beforeSend);
  107. return;
  108. }
  109. if(settings.url) {
  110. // override with url if specified
  111. module.debug('Using specified url', url);
  112. url = module.add.urlData( settings.url );
  113. }
  114. else {
  115. // otherwise find url from api endpoints
  116. url = module.add.urlData( module.get.templateURL() );
  117. module.debug('API url resolved to', url);
  118. }
  119. // exit conditions reached, missing url parameters
  120. if( !url ) {
  121. module.error(error.missingURL);
  122. return;
  123. }
  124. // add loading state
  125. module.set.loading();
  126. // look for jQuery ajax parameters in settings
  127. ajaxSettings = $.extend(true, {}, settings, {
  128. type : settings.method || settings.type,
  129. data : data,
  130. url : url,
  131. beforeSend : settings.beforeXHR,
  132. success : function() {},
  133. failure : function() {},
  134. complete : function() {}
  135. });
  136. module.verbose('Creating AJAX request with settings', ajaxSettings);
  137. // request provides a wrapper around xhr
  138. module.request = module.create.request();
  139. module.xhr = module.create.xhr();
  140. },
  141. is: {
  142. loading: function() {
  143. return (module.request && module.request.state() == 'pending');
  144. }
  145. },
  146. was: {
  147. succesful: function() {
  148. return (module.request && module.request.state() == 'resolved');
  149. },
  150. failure: function() {
  151. return (module.request && module.request.state() == 'rejected');
  152. },
  153. complete: function() {
  154. return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
  155. }
  156. },
  157. add: {
  158. urlData: function(url, urlData) {
  159. var
  160. urlVariables
  161. ;
  162. if(url) {
  163. urlVariables = url.match(settings.regExpTemplate);
  164. urlData = urlData || settings.urlData;
  165. if(urlVariables) {
  166. module.debug('Looking for URL variables', urlVariables);
  167. $.each(urlVariables, function(index, templateValue){
  168. var
  169. term = templateValue.substr( 2, templateValue.length - 3),
  170. termValue = ($.isPlainObject(urlData) && urlData[term] !== undefined)
  171. ? urlData[term]
  172. : ($module.data(term) !== undefined)
  173. ? $module.data(term)
  174. : urlData[term]
  175. ;
  176. module.verbose('Looking for variable', term);
  177. // remove optional value
  178. if(termValue === false) {
  179. module.debug('Removing variable from URL', urlVariables);
  180. url = url.replace('/' + templateValue, '');
  181. }
  182. // undefined condition
  183. else if(termValue === undefined || !termValue) {
  184. module.error(error.missingParameter, term, url);
  185. url = false;
  186. return false;
  187. }
  188. else {
  189. url = url.replace(templateValue, termValue);
  190. }
  191. });
  192. }
  193. }
  194. return url;
  195. }
  196. },
  197. event: {
  198. xhr: {
  199. always: function() {
  200. // calculate if loading time was below minimum threshold
  201. },
  202. done: function(response) {
  203. var
  204. context = this,
  205. elapsedTime = (new Date().getTime() - time),
  206. loadingDelay = (settings.loadingDuration - elapsedTime >= 0)
  207. ? settings.loadingDuration - elapsedTime
  208. : 0
  209. ;
  210. setTimeout(function(){
  211. module.request.resolveWith(context, [response]);
  212. }, loadingDelay);
  213. },
  214. fail: function(xhr, status, httpMessage) {
  215. var
  216. context = this,
  217. elapsedTime = (new Date().getTime() - time),
  218. loadingDelay = (settings.loadingDuration - elapsedTime >= 0)
  219. ? settings.loadingDuration - elapsedTime
  220. : 0
  221. ;
  222. // page triggers abort on navigation, dont show error
  223. if(status !== 'abort') {
  224. setTimeout(function(){
  225. module.request.rejectWith(context, [xhr, status, httpMessage]);
  226. }, settings.loadingDelay);
  227. }
  228. else {
  229. module.reset();
  230. }
  231. }
  232. },
  233. request: {
  234. complete: function() {
  235. module.remove.loading();
  236. $.proxy(settings.complete, $context)();
  237. },
  238. done: function(response) {
  239. module.debug('API request received', response);
  240. if(settings.dataType == 'json') {
  241. module.debug('Checking JSON', settings.successTest, response);
  242. if( $.isFunction(settings.successTest) && settings.success(response) ) {
  243. $.proxy(settings.success, $context)(response, $module);
  244. }
  245. else {
  246. $.proxy(settings.failure, $context)(response, $module);
  247. }
  248. }
  249. else {
  250. $.proxy(settings.success, $context)(response, $module);
  251. }
  252. },
  253. error: function(xhr, status, httpMessage) {
  254. var
  255. errorMessage = (settings.error[status] !== undefined)
  256. ? settings.error[status]
  257. : httpMessage,
  258. response
  259. ;
  260. // let em know unless request aborted
  261. if(xhr !== undefined) {
  262. // readyState 4 = done, anything less is not really sent
  263. if(xhr.readyState !== undefined && xhr.readyState == 4) {
  264. // if http status code returned and json returned error, look for it
  265. if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
  266. module.error(error.statusMessage + httpMessage);
  267. }
  268. else {
  269. if(status == 'error' && settings.dataType == 'json') {
  270. try {
  271. response = $.parseJSON(xhr.responseText);
  272. if(response && response.error !== undefined) {
  273. errorMessage = response.error;
  274. }
  275. }
  276. catch(er) {
  277. module.error(error.JSONParse);
  278. }
  279. }
  280. }
  281. module.remove.loading();
  282. module.set.error();
  283. // show error state only for duration specified in settings
  284. if(settings.errorDuration) {
  285. setTimeout(module.remove.error, settings.errorDuration);
  286. }
  287. module.debug('API Request error:', errorMessage);
  288. $.proxy(settings.failure, $context)(errorMessage, this);
  289. }
  290. else {
  291. module.debug('Request Aborted (Most likely caused by page change)');
  292. }
  293. }
  294. }
  295. }
  296. },
  297. create: {
  298. request: function() {
  299. return $.Deferred()
  300. .always(module.event.request.complete)
  301. .done(module.event.request.done)
  302. .fail(module.event.request.error)
  303. ;
  304. },
  305. xhr: function() {
  306. $.ajax(ajaxSettings)
  307. .always(module.event.xhr.always)
  308. .done(module.event.xhr.done)
  309. .fail(module.event.xhr.fail)
  310. ;
  311. }
  312. },
  313. set: {
  314. error: function() {
  315. module.verbose('Adding error state to element', $context);
  316. $context.addClass(className.error);
  317. },
  318. loading: function() {
  319. module.verbose('Adding loading state to element', $context);
  320. $context.addClass(className.loading);
  321. }
  322. },
  323. remove: {
  324. error: function() {
  325. module.verbose('Removing error state from element', $context);
  326. $context.removeClass(className.error);
  327. },
  328. loading: function() {
  329. module.verbose('Removing loading state from element', $context);
  330. $context.removeClass(className.loading);
  331. }
  332. },
  333. get: {
  334. request: function() {
  335. return module.request;
  336. },
  337. xhr: function() {
  338. return module.xhr;
  339. },
  340. settings: function() {
  341. return $.proxy(settings.beforeSend, $module)(settings);
  342. },
  343. defaultData: function() {
  344. var
  345. data = {}
  346. ;
  347. if( !$.isWindow(element) ) {
  348. if( $module.is('input') ) {
  349. data.value = $module.val();
  350. }
  351. else {
  352. data.text = $module.text();
  353. }
  354. }
  355. return data;
  356. },
  357. event: function() {
  358. if( $.isWindow(element) || settings.on == 'now' ) {
  359. module.debug('API called without element, no events attached');
  360. return false;
  361. }
  362. else if(settings.on == 'auto') {
  363. if( $module.is('input') ) {
  364. return (element.oninput !== undefined)
  365. ? 'input'
  366. : (element.onpropertychange !== undefined)
  367. ? 'propertychange'
  368. : 'keyup'
  369. ;
  370. }
  371. else {
  372. return 'click';
  373. }
  374. }
  375. else {
  376. return settings.on;
  377. }
  378. },
  379. formData: function() {
  380. var
  381. formData
  382. ;
  383. if( $(this).toJSON() === undefined ) {
  384. module.error(error.missingSerialize);
  385. return;
  386. }
  387. formData = $form.toJSON();
  388. module.debug('Retrieving form data', formData);
  389. return $form.toJSON();
  390. },
  391. templateURL: function(action) {
  392. var
  393. url
  394. ;
  395. action = action || $module.data(settings.metadata.action) || settings.action || false;
  396. if(action) {
  397. module.debug('Looking up url for action', action);
  398. if(settings.api[action] !== undefined) {
  399. url = settings.api[action];
  400. module.debug('Found template url', url);
  401. }
  402. else {
  403. module.error(error.missingAction, settings.action);
  404. }
  405. }
  406. return url;
  407. }
  408. },
  409. // reset state
  410. reset: function() {
  411. module.remove.error();
  412. module.remove.loading();
  413. },
  414. setting: function(name, value) {
  415. if( $.isPlainObject(name) ) {
  416. $.extend(true, settings, name);
  417. }
  418. else if(value !== undefined) {
  419. settings[name] = value;
  420. }
  421. else {
  422. return settings[name];
  423. }
  424. },
  425. internal: function(name, value) {
  426. if( $.isPlainObject(name) ) {
  427. $.extend(true, module, name);
  428. }
  429. else if(value !== undefined) {
  430. module[name] = value;
  431. }
  432. else {
  433. return module[name];
  434. }
  435. },
  436. debug: function() {
  437. if(settings.debug) {
  438. if(settings.performance) {
  439. module.performance.log(arguments);
  440. }
  441. else {
  442. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  443. module.debug.apply(console, arguments);
  444. }
  445. }
  446. },
  447. verbose: function() {
  448. if(settings.verbose && settings.debug) {
  449. if(settings.performance) {
  450. module.performance.log(arguments);
  451. }
  452. else {
  453. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  454. module.verbose.apply(console, arguments);
  455. }
  456. }
  457. },
  458. error: function() {
  459. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  460. module.error.apply(console, arguments);
  461. },
  462. performance: {
  463. log: function(message) {
  464. var
  465. currentTime,
  466. executionTime,
  467. previousTime
  468. ;
  469. if(settings.performance) {
  470. currentTime = new Date().getTime();
  471. previousTime = time || currentTime;
  472. executionTime = currentTime - previousTime;
  473. time = currentTime;
  474. performance.push({
  475. 'Element' : element,
  476. 'Name' : message[0],
  477. 'Arguments' : [].slice.call(message, 1) || '',
  478. 'Execution Time' : executionTime
  479. });
  480. }
  481. clearTimeout(module.performance.timer);
  482. module.performance.timer = setTimeout(module.performance.display, 100);
  483. },
  484. display: function() {
  485. var
  486. title = settings.name + ':',
  487. totalTime = 0
  488. ;
  489. time = false;
  490. clearTimeout(module.performance.timer);
  491. $.each(performance, function(index, data) {
  492. totalTime += data['Execution Time'];
  493. });
  494. title += ' ' + totalTime + 'ms';
  495. if(moduleSelector) {
  496. title += ' \'' + moduleSelector + '\'';
  497. }
  498. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  499. console.groupCollapsed(title);
  500. if(console.table) {
  501. console.table(performance);
  502. }
  503. else {
  504. $.each(performance, function(index, data) {
  505. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  506. });
  507. }
  508. console.groupEnd();
  509. }
  510. performance = [];
  511. }
  512. },
  513. invoke: function(query, passedArguments, context) {
  514. var
  515. maxDepth,
  516. found,
  517. response
  518. ;
  519. passedArguments = passedArguments || queryArguments;
  520. context = element || context;
  521. if(typeof query == 'string' && instance !== undefined) {
  522. query = query.split(/[\. ]/);
  523. maxDepth = query.length - 1;
  524. $.each(query, function(depth, value) {
  525. var camelCaseValue = (depth != maxDepth)
  526. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  527. : query
  528. ;
  529. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  530. instance = instance[value];
  531. }
  532. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  533. instance = instance[camelCaseValue];
  534. }
  535. else if( instance[value] !== undefined ) {
  536. found = instance[value];
  537. return false;
  538. }
  539. else if( instance[camelCaseValue] !== undefined ) {
  540. found = instance[camelCaseValue];
  541. return false;
  542. }
  543. else {
  544. module.error(error.method, query);
  545. return false;
  546. }
  547. });
  548. }
  549. if ( $.isFunction( found ) ) {
  550. response = found.apply(context, passedArguments);
  551. }
  552. else if(found !== undefined) {
  553. response = found;
  554. }
  555. if($.isArray(returnedValue)) {
  556. returnedValue.push(response);
  557. }
  558. else if(returnedValue !== undefined) {
  559. returnedValue = [returnedValue, response];
  560. }
  561. else if(response !== undefined) {
  562. returnedValue = response;
  563. }
  564. return found;
  565. }
  566. };
  567. if(methodInvoked) {
  568. if(instance === undefined) {
  569. module.initialize();
  570. }
  571. module.invoke(query);
  572. }
  573. else {
  574. if(instance !== undefined) {
  575. module.destroy();
  576. }
  577. module.initialize();
  578. }
  579. })
  580. ;
  581. return (returnedValue !== undefined)
  582. ? returnedValue
  583. : this
  584. ;
  585. };
  586. $.api.settings = {
  587. name : 'API',
  588. namespace : 'api',
  589. debug : true,
  590. verbose : true,
  591. performance : true,
  592. // event binding
  593. on : 'auto',
  594. filter : '.disabled, .loading',
  595. context : false,
  596. stateContext : false,
  597. // templating
  598. action : false,
  599. regExpTemplate : /\{\$([A-z]+)\}/g,
  600. // data
  601. url : false,
  602. urlData : false,
  603. serializeForm : false,
  604. // ui
  605. defaultData : true,
  606. throttle : 100,
  607. allowMultiple : false,
  608. // state
  609. loadingDuration : 1000,
  610. errorDuration : 2000,
  611. // jQ ajax
  612. method : 'get',
  613. data : {},
  614. dataType : 'json',
  615. cache : true,
  616. // callbacks
  617. beforeSend : function(settings) { return settings; },
  618. beforeXHR : function(xhr) {},
  619. success : function(response) {},
  620. successText : function(response) { return true; },
  621. complete : function(response) {},
  622. failure : function(response) {},
  623. // errors
  624. error : {
  625. beforeSend : 'The before send function has aborted the request',
  626. error : 'There was an error with your request',
  627. exitConditions : 'API Request Aborted. Exit conditions met',
  628. JSONParse : 'JSON could not be parsed during error handling',
  629. missingSerialize : 'Serializing a Form requires toJSON to be included',
  630. missingAction : 'API action used but no url was defined',
  631. missingParameter : 'Missing an essential URL parameter: ',
  632. missingURL : 'No URL specified for api event',
  633. parseError : 'There was an error parsing your request',
  634. statusMessage : 'Server gave an error: ',
  635. timeout : 'Your request timed out'
  636. },
  637. className: {
  638. loading : 'loading',
  639. error : 'error'
  640. },
  641. selector: {
  642. form: 'form'
  643. },
  644. metadata: {
  645. action : 'action',
  646. request : 'request',
  647. xhr : 'xhr'
  648. }
  649. };
  650. $.api.settings.api = {};
  651. })( jQuery, window , document );