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.

718 lines
22 KiB

  1. /*
  2. * # Semantic - API
  3. * http://github.com/semantic-org/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. if( $.isFunction(settings.successTest) ) {
  242. module.debug('Checking JSON', settings.successTest, response);
  243. if( settings.success(response) ) {
  244. $.proxy(settings.success, $context)(response, $module);
  245. }
  246. else {
  247. module.debug('JSON test specified by user and response failed', response);
  248. $.proxy(settings.failure, $context)(response, $module);
  249. }
  250. }
  251. else {
  252. $.proxy(settings.success, $context)(response, $module);
  253. }
  254. }
  255. else {
  256. $.proxy(settings.success, $context)(response, $module);
  257. }
  258. },
  259. error: function(xhr, status, httpMessage) {
  260. var
  261. errorMessage = (settings.error[status] !== undefined)
  262. ? settings.error[status]
  263. : httpMessage,
  264. response
  265. ;
  266. // let em know unless request aborted
  267. if(xhr !== undefined) {
  268. // readyState 4 = done, anything less is not really sent
  269. if(xhr.readyState !== undefined && xhr.readyState == 4) {
  270. // if http status code returned and json returned error, look for it
  271. if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
  272. module.error(error.statusMessage + httpMessage);
  273. }
  274. else {
  275. if(status == 'error' && settings.dataType == 'json') {
  276. try {
  277. response = $.parseJSON(xhr.responseText);
  278. if(response && response.error !== undefined) {
  279. errorMessage = response.error;
  280. }
  281. }
  282. catch(er) {
  283. module.error(error.JSONParse);
  284. }
  285. }
  286. }
  287. module.remove.loading();
  288. module.set.error();
  289. // show error state only for duration specified in settings
  290. if(settings.errorDuration) {
  291. setTimeout(module.remove.error, settings.errorDuration);
  292. }
  293. module.debug('API Request error:', errorMessage);
  294. $.proxy(settings.failure, $context)(errorMessage, this);
  295. }
  296. else {
  297. module.debug('Request Aborted (Most likely caused by page change)');
  298. }
  299. }
  300. }
  301. }
  302. },
  303. create: {
  304. request: function() {
  305. return $.Deferred()
  306. .always(module.event.request.complete)
  307. .done(module.event.request.done)
  308. .fail(module.event.request.error)
  309. ;
  310. },
  311. xhr: function() {
  312. $.ajax(ajaxSettings)
  313. .always(module.event.xhr.always)
  314. .done(module.event.xhr.done)
  315. .fail(module.event.xhr.fail)
  316. ;
  317. }
  318. },
  319. set: {
  320. error: function() {
  321. module.verbose('Adding error state to element', $context);
  322. $context.addClass(className.error);
  323. },
  324. loading: function() {
  325. module.verbose('Adding loading state to element', $context);
  326. $context.addClass(className.loading);
  327. }
  328. },
  329. remove: {
  330. error: function() {
  331. module.verbose('Removing error state from element', $context);
  332. $context.removeClass(className.error);
  333. },
  334. loading: function() {
  335. module.verbose('Removing loading state from element', $context);
  336. $context.removeClass(className.loading);
  337. }
  338. },
  339. get: {
  340. request: function() {
  341. return module.request;
  342. },
  343. xhr: function() {
  344. return module.xhr;
  345. },
  346. settings: function() {
  347. return $.proxy(settings.beforeSend, $module)(settings);
  348. },
  349. defaultData: function() {
  350. var
  351. data = {}
  352. ;
  353. if( !$.isWindow(element) ) {
  354. if( $module.is('input') ) {
  355. data.value = $module.val();
  356. }
  357. else {
  358. data.text = $module.text();
  359. }
  360. }
  361. return data;
  362. },
  363. event: function() {
  364. if( $.isWindow(element) || settings.on == 'now' ) {
  365. module.debug('API called without element, no events attached');
  366. return false;
  367. }
  368. else if(settings.on == 'auto') {
  369. if( $module.is('input') ) {
  370. return (element.oninput !== undefined)
  371. ? 'input'
  372. : (element.onpropertychange !== undefined)
  373. ? 'propertychange'
  374. : 'keyup'
  375. ;
  376. }
  377. else {
  378. return 'click';
  379. }
  380. }
  381. else {
  382. return settings.on;
  383. }
  384. },
  385. formData: function() {
  386. var
  387. formData
  388. ;
  389. if( $(this).toJSON() === undefined ) {
  390. module.error(error.missingSerialize);
  391. return;
  392. }
  393. formData = $form.toJSON();
  394. module.debug('Retrieving form data', formData);
  395. return $form.toJSON();
  396. },
  397. templateURL: function(action) {
  398. var
  399. url
  400. ;
  401. action = action || $module.data(settings.metadata.action) || settings.action || false;
  402. if(action) {
  403. module.debug('Looking up url for action', action);
  404. if(settings.api[action] !== undefined) {
  405. url = settings.api[action];
  406. module.debug('Found template url', url);
  407. }
  408. else {
  409. module.error(error.missingAction, settings.action);
  410. }
  411. }
  412. return url;
  413. }
  414. },
  415. // reset state
  416. reset: function() {
  417. module.remove.error();
  418. module.remove.loading();
  419. },
  420. setting: function(name, value) {
  421. if( $.isPlainObject(name) ) {
  422. $.extend(true, settings, name);
  423. }
  424. else if(value !== undefined) {
  425. settings[name] = value;
  426. }
  427. else {
  428. return settings[name];
  429. }
  430. },
  431. internal: function(name, value) {
  432. if( $.isPlainObject(name) ) {
  433. $.extend(true, module, name);
  434. }
  435. else if(value !== undefined) {
  436. module[name] = value;
  437. }
  438. else {
  439. return module[name];
  440. }
  441. },
  442. debug: function() {
  443. if(settings.debug) {
  444. if(settings.performance) {
  445. module.performance.log(arguments);
  446. }
  447. else {
  448. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  449. module.debug.apply(console, arguments);
  450. }
  451. }
  452. },
  453. verbose: function() {
  454. if(settings.verbose && settings.debug) {
  455. if(settings.performance) {
  456. module.performance.log(arguments);
  457. }
  458. else {
  459. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  460. module.verbose.apply(console, arguments);
  461. }
  462. }
  463. },
  464. error: function() {
  465. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  466. module.error.apply(console, arguments);
  467. },
  468. performance: {
  469. log: function(message) {
  470. var
  471. currentTime,
  472. executionTime,
  473. previousTime
  474. ;
  475. if(settings.performance) {
  476. currentTime = new Date().getTime();
  477. previousTime = time || currentTime;
  478. executionTime = currentTime - previousTime;
  479. time = currentTime;
  480. performance.push({
  481. 'Element' : element,
  482. 'Name' : message[0],
  483. 'Arguments' : [].slice.call(message, 1) || '',
  484. 'Execution Time' : executionTime
  485. });
  486. }
  487. clearTimeout(module.performance.timer);
  488. module.performance.timer = setTimeout(module.performance.display, 100);
  489. },
  490. display: function() {
  491. var
  492. title = settings.name + ':',
  493. totalTime = 0
  494. ;
  495. time = false;
  496. clearTimeout(module.performance.timer);
  497. $.each(performance, function(index, data) {
  498. totalTime += data['Execution Time'];
  499. });
  500. title += ' ' + totalTime + 'ms';
  501. if(moduleSelector) {
  502. title += ' \'' + moduleSelector + '\'';
  503. }
  504. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  505. console.groupCollapsed(title);
  506. if(console.table) {
  507. console.table(performance);
  508. }
  509. else {
  510. $.each(performance, function(index, data) {
  511. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  512. });
  513. }
  514. console.groupEnd();
  515. }
  516. performance = [];
  517. }
  518. },
  519. invoke: function(query, passedArguments, context) {
  520. var
  521. maxDepth,
  522. found,
  523. response
  524. ;
  525. passedArguments = passedArguments || queryArguments;
  526. context = element || context;
  527. if(typeof query == 'string' && instance !== undefined) {
  528. query = query.split(/[\. ]/);
  529. maxDepth = query.length - 1;
  530. $.each(query, function(depth, value) {
  531. var camelCaseValue = (depth != maxDepth)
  532. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  533. : query
  534. ;
  535. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  536. instance = instance[value];
  537. }
  538. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  539. instance = instance[camelCaseValue];
  540. }
  541. else if( instance[value] !== undefined ) {
  542. found = instance[value];
  543. return false;
  544. }
  545. else if( instance[camelCaseValue] !== undefined ) {
  546. found = instance[camelCaseValue];
  547. return false;
  548. }
  549. else {
  550. module.error(error.method, query);
  551. return false;
  552. }
  553. });
  554. }
  555. if ( $.isFunction( found ) ) {
  556. response = found.apply(context, passedArguments);
  557. }
  558. else if(found !== undefined) {
  559. response = found;
  560. }
  561. if($.isArray(returnedValue)) {
  562. returnedValue.push(response);
  563. }
  564. else if(returnedValue !== undefined) {
  565. returnedValue = [returnedValue, response];
  566. }
  567. else if(response !== undefined) {
  568. returnedValue = response;
  569. }
  570. return found;
  571. }
  572. };
  573. if(methodInvoked) {
  574. if(instance === undefined) {
  575. module.initialize();
  576. }
  577. module.invoke(query);
  578. }
  579. else {
  580. if(instance !== undefined) {
  581. module.destroy();
  582. }
  583. module.initialize();
  584. }
  585. })
  586. ;
  587. return (returnedValue !== undefined)
  588. ? returnedValue
  589. : this
  590. ;
  591. };
  592. $.api.settings = {
  593. name : 'API',
  594. namespace : 'api',
  595. debug : true,
  596. verbose : true,
  597. performance : true,
  598. // event binding
  599. on : 'auto',
  600. filter : '.disabled, .loading',
  601. context : false,
  602. stateContext : false,
  603. // templating
  604. action : false,
  605. regExpTemplate : /\{\$([A-z]+)\}/g,
  606. // data
  607. url : false,
  608. urlData : false,
  609. serializeForm : false,
  610. // ui
  611. defaultData : true,
  612. throttle : 100,
  613. allowMultiple : false,
  614. // state
  615. loadingDuration : 1000,
  616. errorDuration : 2000,
  617. // jQ ajax
  618. method : 'get',
  619. data : {},
  620. dataType : 'json',
  621. cache : true,
  622. // callbacks
  623. beforeSend : function(settings) { return settings; },
  624. beforeXHR : function(xhr) {},
  625. success : function(response) {},
  626. successText : function(response) { return true; },
  627. complete : function(response) {},
  628. failure : function(response) {},
  629. // errors
  630. error : {
  631. beforeSend : 'The before send function has aborted the request',
  632. error : 'There was an error with your request',
  633. exitConditions : 'API Request Aborted. Exit conditions met',
  634. JSONParse : 'JSON could not be parsed during error handling',
  635. missingSerialize : 'Serializing a Form requires toJSON to be included',
  636. missingAction : 'API action used but no url was defined',
  637. missingParameter : 'Missing an essential URL parameter: ',
  638. missingURL : 'No URL specified for api event',
  639. parseError : 'There was an error parsing your request',
  640. statusMessage : 'Server gave an error: ',
  641. timeout : 'Your request timed out'
  642. },
  643. className: {
  644. loading : 'loading',
  645. error : 'error'
  646. },
  647. selector: {
  648. form: 'form'
  649. },
  650. metadata: {
  651. action : 'action',
  652. request : 'request',
  653. xhr : 'xhr'
  654. }
  655. };
  656. $.api.settings.api = {};
  657. })( jQuery, window , document );