api.js 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. /*!
  2. * # Fomantic-UI - API
  3. * http://github.com/fomantic/Fomantic-UI/
  4. *
  5. *
  6. * Released under the MIT license
  7. * http://opensource.org/licenses/MIT
  8. *
  9. */
  10. ;(function ($, window, document, undefined) {
  11. 'use strict';
  12. $.isWindow = $.isWindow || function(obj) {
  13. return obj != null && obj === obj.window;
  14. };
  15. window = (typeof window != 'undefined' && window.Math == Math)
  16. ? window
  17. : (typeof self != 'undefined' && self.Math == Math)
  18. ? self
  19. : Function('return this')()
  20. ;
  21. $.api = $.fn.api = function(parameters) {
  22. var
  23. // use window context if none specified
  24. $allModules = $.isFunction(this)
  25. ? $(window)
  26. : $(this),
  27. moduleSelector = $allModules.selector || '',
  28. time = new Date().getTime(),
  29. performance = [],
  30. query = arguments[0],
  31. methodInvoked = (typeof query == 'string'),
  32. queryArguments = [].slice.call(arguments, 1),
  33. returnedValue
  34. ;
  35. $allModules
  36. .each(function() {
  37. var
  38. settings = ( $.isPlainObject(parameters) )
  39. ? $.extend(true, {}, $.fn.api.settings, parameters)
  40. : $.extend({}, $.fn.api.settings),
  41. // internal aliases
  42. namespace = settings.namespace,
  43. metadata = settings.metadata,
  44. selector = settings.selector,
  45. error = settings.error,
  46. className = settings.className,
  47. // define namespaces for modules
  48. eventNamespace = '.' + namespace,
  49. moduleNamespace = 'module-' + namespace,
  50. // element that creates request
  51. $module = $(this),
  52. $form = $module.closest(selector.form),
  53. // context used for state
  54. $context = (settings.stateContext)
  55. ? $(settings.stateContext)
  56. : $module,
  57. // request details
  58. ajaxSettings,
  59. requestSettings,
  60. url,
  61. data,
  62. requestStartTime,
  63. // standard module
  64. element = this,
  65. context = $context[0],
  66. instance = $module.data(moduleNamespace),
  67. module
  68. ;
  69. module = {
  70. initialize: function() {
  71. if(!methodInvoked) {
  72. module.bind.events();
  73. }
  74. module.instantiate();
  75. },
  76. instantiate: function() {
  77. module.verbose('Storing instance of module', module);
  78. instance = module;
  79. $module
  80. .data(moduleNamespace, instance)
  81. ;
  82. },
  83. destroy: function() {
  84. module.verbose('Destroying previous module for', element);
  85. $module
  86. .removeData(moduleNamespace)
  87. .off(eventNamespace)
  88. ;
  89. },
  90. bind: {
  91. events: function() {
  92. var
  93. triggerEvent = module.get.event()
  94. ;
  95. if( triggerEvent ) {
  96. module.verbose('Attaching API events to element', triggerEvent);
  97. $module
  98. .on(triggerEvent + eventNamespace, module.event.trigger)
  99. ;
  100. }
  101. else if(settings.on == 'now') {
  102. module.debug('Querying API endpoint immediately');
  103. module.query();
  104. }
  105. }
  106. },
  107. decode: {
  108. json: function(response) {
  109. if(response !== undefined && typeof response == 'string') {
  110. try {
  111. response = JSON.parse(response);
  112. }
  113. catch(e) {
  114. // isnt json string
  115. }
  116. }
  117. return response;
  118. }
  119. },
  120. read: {
  121. cachedResponse: function(url) {
  122. var
  123. response
  124. ;
  125. if(window.Storage === undefined) {
  126. module.error(error.noStorage);
  127. return;
  128. }
  129. response = sessionStorage.getItem(url);
  130. module.debug('Using cached response', url, response);
  131. response = module.decode.json(response);
  132. return response;
  133. }
  134. },
  135. write: {
  136. cachedResponse: function(url, response) {
  137. if(response && response === '') {
  138. module.debug('Response empty, not caching', response);
  139. return;
  140. }
  141. if(window.Storage === undefined) {
  142. module.error(error.noStorage);
  143. return;
  144. }
  145. if( $.isPlainObject(response) ) {
  146. response = JSON.stringify(response);
  147. }
  148. sessionStorage.setItem(url, response);
  149. module.verbose('Storing cached response for url', url, response);
  150. }
  151. },
  152. query: function() {
  153. if(module.is.disabled()) {
  154. module.debug('Element is disabled API request aborted');
  155. return;
  156. }
  157. if(module.is.loading()) {
  158. if(settings.interruptRequests) {
  159. module.debug('Interrupting previous request');
  160. module.abort();
  161. }
  162. else {
  163. module.debug('Cancelling request, previous request is still pending');
  164. return;
  165. }
  166. }
  167. // pass element metadata to url (value, text)
  168. if(settings.defaultData) {
  169. $.extend(true, settings.urlData, module.get.defaultData());
  170. }
  171. // Add form content
  172. if(settings.serializeForm) {
  173. settings.data = module.add.formData(settings.data);
  174. }
  175. // call beforesend and get any settings changes
  176. requestSettings = module.get.settings();
  177. // check if before send cancelled request
  178. if(requestSettings === false) {
  179. module.cancelled = true;
  180. module.error(error.beforeSend);
  181. return;
  182. }
  183. else {
  184. module.cancelled = false;
  185. }
  186. // get url
  187. url = module.get.templatedURL();
  188. if(!url && !module.is.mocked()) {
  189. module.error(error.missingURL);
  190. return;
  191. }
  192. // replace variables
  193. url = module.add.urlData( url );
  194. // missing url parameters
  195. if( !url && !module.is.mocked()) {
  196. return;
  197. }
  198. requestSettings.url = settings.base + url;
  199. // look for jQuery ajax parameters in settings
  200. ajaxSettings = $.extend(true, {}, settings, {
  201. type : settings.method || settings.type,
  202. data : data,
  203. url : settings.base + url,
  204. beforeSend : settings.beforeXHR,
  205. success : function() {},
  206. failure : function() {},
  207. complete : function() {}
  208. });
  209. module.debug('Querying URL', ajaxSettings.url);
  210. module.verbose('Using AJAX settings', ajaxSettings);
  211. if(settings.cache === 'local' && module.read.cachedResponse(url)) {
  212. module.debug('Response returned from local cache');
  213. module.request = module.create.request();
  214. module.request.resolveWith(context, [ module.read.cachedResponse(url) ]);
  215. return;
  216. }
  217. if( !settings.throttle ) {
  218. module.debug('Sending request', data, ajaxSettings.method);
  219. module.send.request();
  220. }
  221. else {
  222. if(!settings.throttleFirstRequest && !module.timer) {
  223. module.debug('Sending request', data, ajaxSettings.method);
  224. module.send.request();
  225. module.timer = setTimeout(function(){}, settings.throttle);
  226. }
  227. else {
  228. module.debug('Throttling request', settings.throttle);
  229. clearTimeout(module.timer);
  230. module.timer = setTimeout(function() {
  231. if(module.timer) {
  232. delete module.timer;
  233. }
  234. module.debug('Sending throttled request', data, ajaxSettings.method);
  235. module.send.request();
  236. }, settings.throttle);
  237. }
  238. }
  239. },
  240. should: {
  241. removeError: function() {
  242. return ( settings.hideError === true || (settings.hideError === 'auto' && !module.is.form()) );
  243. }
  244. },
  245. is: {
  246. disabled: function() {
  247. return ($module.filter(selector.disabled).length > 0);
  248. },
  249. expectingJSON: function() {
  250. return settings.dataType === 'json' || settings.dataType === 'jsonp';
  251. },
  252. form: function() {
  253. return $module.is('form') || $context.is('form');
  254. },
  255. mocked: function() {
  256. return (settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync);
  257. },
  258. input: function() {
  259. return $module.is('input');
  260. },
  261. loading: function() {
  262. return (module.request)
  263. ? (module.request.state() == 'pending')
  264. : false
  265. ;
  266. },
  267. abortedRequest: function(xhr) {
  268. if(xhr && xhr.readyState !== undefined && xhr.readyState === 0) {
  269. module.verbose('XHR request determined to be aborted');
  270. return true;
  271. }
  272. else {
  273. module.verbose('XHR request was not aborted');
  274. return false;
  275. }
  276. },
  277. validResponse: function(response) {
  278. if( (!module.is.expectingJSON()) || !$.isFunction(settings.successTest) ) {
  279. module.verbose('Response is not JSON, skipping validation', settings.successTest, response);
  280. return true;
  281. }
  282. module.debug('Checking JSON returned success', settings.successTest, response);
  283. if( settings.successTest(response) ) {
  284. module.debug('Response passed success test', response);
  285. return true;
  286. }
  287. else {
  288. module.debug('Response failed success test', response);
  289. return false;
  290. }
  291. }
  292. },
  293. was: {
  294. cancelled: function() {
  295. return (module.cancelled || false);
  296. },
  297. succesful: function() {
  298. module.verbose('This behavior will be deleted due to typo. Use "was successful" instead.');
  299. return module.was.successful();
  300. },
  301. successful: function() {
  302. return (module.request && module.request.state() == 'resolved');
  303. },
  304. failure: function() {
  305. return (module.request && module.request.state() == 'rejected');
  306. },
  307. complete: function() {
  308. return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
  309. }
  310. },
  311. add: {
  312. urlData: function(url, urlData) {
  313. var
  314. requiredVariables,
  315. optionalVariables
  316. ;
  317. if(url) {
  318. requiredVariables = url.match(settings.regExp.required);
  319. optionalVariables = url.match(settings.regExp.optional);
  320. urlData = urlData || settings.urlData;
  321. if(requiredVariables) {
  322. module.debug('Looking for required URL variables', requiredVariables);
  323. $.each(requiredVariables, function(index, templatedString) {
  324. var
  325. // allow legacy {$var} style
  326. variable = (templatedString.indexOf('$') !== -1)
  327. ? templatedString.substr(2, templatedString.length - 3)
  328. : templatedString.substr(1, templatedString.length - 2),
  329. value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
  330. ? urlData[variable]
  331. : ($module.data(variable) !== undefined)
  332. ? $module.data(variable)
  333. : ($context.data(variable) !== undefined)
  334. ? $context.data(variable)
  335. : urlData[variable]
  336. ;
  337. // remove value
  338. if(value === undefined) {
  339. module.error(error.requiredParameter, variable, url);
  340. url = false;
  341. return false;
  342. }
  343. else {
  344. module.verbose('Found required variable', variable, value);
  345. value = (settings.encodeParameters)
  346. ? module.get.urlEncodedValue(value)
  347. : value
  348. ;
  349. url = url.replace(templatedString, value);
  350. }
  351. });
  352. }
  353. if(optionalVariables) {
  354. module.debug('Looking for optional URL variables', requiredVariables);
  355. $.each(optionalVariables, function(index, templatedString) {
  356. var
  357. // allow legacy {/$var} style
  358. variable = (templatedString.indexOf('$') !== -1)
  359. ? templatedString.substr(3, templatedString.length - 4)
  360. : templatedString.substr(2, templatedString.length - 3),
  361. value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
  362. ? urlData[variable]
  363. : ($module.data(variable) !== undefined)
  364. ? $module.data(variable)
  365. : ($context.data(variable) !== undefined)
  366. ? $context.data(variable)
  367. : urlData[variable]
  368. ;
  369. // optional replacement
  370. if(value !== undefined) {
  371. module.verbose('Optional variable Found', variable, value);
  372. url = url.replace(templatedString, value);
  373. }
  374. else {
  375. module.verbose('Optional variable not found', variable);
  376. // remove preceding slash if set
  377. if(url.indexOf('/' + templatedString) !== -1) {
  378. url = url.replace('/' + templatedString, '');
  379. }
  380. else {
  381. url = url.replace(templatedString, '');
  382. }
  383. }
  384. });
  385. }
  386. }
  387. return url;
  388. },
  389. formData: function(data) {
  390. var
  391. canSerialize = ($.fn.serializeObject !== undefined),
  392. formData = (canSerialize)
  393. ? $form.serializeObject()
  394. : $form.serialize(),
  395. hasOtherData
  396. ;
  397. data = data || settings.data;
  398. hasOtherData = $.isPlainObject(data);
  399. if(hasOtherData) {
  400. if(canSerialize) {
  401. module.debug('Extending existing data with form data', data, formData);
  402. data = $.extend(true, {}, data, formData);
  403. }
  404. else {
  405. module.error(error.missingSerialize);
  406. module.debug('Cant extend data. Replacing data with form data', data, formData);
  407. data = formData;
  408. }
  409. }
  410. else {
  411. module.debug('Adding form data', formData);
  412. data = formData;
  413. }
  414. return data;
  415. }
  416. },
  417. send: {
  418. request: function() {
  419. module.set.loading();
  420. module.request = module.create.request();
  421. if( module.is.mocked() ) {
  422. module.mockedXHR = module.create.mockedXHR();
  423. }
  424. else {
  425. module.xhr = module.create.xhr();
  426. }
  427. settings.onRequest.call(context, module.request, module.xhr);
  428. }
  429. },
  430. event: {
  431. trigger: function(event) {
  432. module.query();
  433. if(event.type == 'submit' || event.type == 'click') {
  434. event.preventDefault();
  435. }
  436. },
  437. xhr: {
  438. always: function() {
  439. // nothing special
  440. },
  441. done: function(response, textStatus, xhr) {
  442. var
  443. context = this,
  444. elapsedTime = (new Date().getTime() - requestStartTime),
  445. timeLeft = (settings.loadingDuration - elapsedTime),
  446. translatedResponse = ( $.isFunction(settings.onResponse) )
  447. ? module.is.expectingJSON() && !settings.rawResponse
  448. ? settings.onResponse.call(context, $.extend(true, {}, response))
  449. : settings.onResponse.call(context, response)
  450. : false
  451. ;
  452. timeLeft = (timeLeft > 0)
  453. ? timeLeft
  454. : 0
  455. ;
  456. if(translatedResponse) {
  457. module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response);
  458. response = translatedResponse;
  459. }
  460. if(timeLeft > 0) {
  461. module.debug('Response completed early delaying state change by', timeLeft);
  462. }
  463. setTimeout(function() {
  464. if( module.is.validResponse(response) ) {
  465. module.request.resolveWith(context, [response, xhr]);
  466. }
  467. else {
  468. module.request.rejectWith(context, [xhr, 'invalid']);
  469. }
  470. }, timeLeft);
  471. },
  472. fail: function(xhr, status, httpMessage) {
  473. var
  474. context = this,
  475. elapsedTime = (new Date().getTime() - requestStartTime),
  476. timeLeft = (settings.loadingDuration - elapsedTime)
  477. ;
  478. timeLeft = (timeLeft > 0)
  479. ? timeLeft
  480. : 0
  481. ;
  482. if(timeLeft > 0) {
  483. module.debug('Response completed early delaying state change by', timeLeft);
  484. }
  485. setTimeout(function() {
  486. if( module.is.abortedRequest(xhr) ) {
  487. module.request.rejectWith(context, [xhr, 'aborted', httpMessage]);
  488. }
  489. else {
  490. module.request.rejectWith(context, [xhr, 'error', status, httpMessage]);
  491. }
  492. }, timeLeft);
  493. }
  494. },
  495. request: {
  496. done: function(response, xhr) {
  497. module.debug('Successful API Response', response);
  498. if(settings.cache === 'local' && url) {
  499. module.write.cachedResponse(url, response);
  500. module.debug('Saving server response locally', module.cache);
  501. }
  502. settings.onSuccess.call(context, response, $module, xhr);
  503. },
  504. complete: function(firstParameter, secondParameter) {
  505. var
  506. xhr,
  507. response
  508. ;
  509. // have to guess callback parameters based on request success
  510. if( module.was.successful() ) {
  511. response = firstParameter;
  512. xhr = secondParameter;
  513. }
  514. else {
  515. xhr = firstParameter;
  516. response = module.get.responseFromXHR(xhr);
  517. }
  518. module.remove.loading();
  519. settings.onComplete.call(context, response, $module, xhr);
  520. },
  521. fail: function(xhr, status, httpMessage) {
  522. var
  523. // pull response from xhr if available
  524. response = module.get.responseFromXHR(xhr),
  525. errorMessage = module.get.errorFromRequest(response, status, httpMessage)
  526. ;
  527. if(status == 'aborted') {
  528. module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage);
  529. settings.onAbort.call(context, status, $module, xhr);
  530. return true;
  531. }
  532. else if(status == 'invalid') {
  533. module.debug('JSON did not pass success test. A server-side error has most likely occurred', response);
  534. }
  535. else if(status == 'error') {
  536. if(xhr !== undefined) {
  537. module.debug('XHR produced a server error', status, httpMessage);
  538. // make sure we have an error to display to console
  539. if( (xhr.status < 200 || xhr.status >= 300) && httpMessage !== undefined && httpMessage !== '') {
  540. module.error(error.statusMessage + httpMessage, ajaxSettings.url);
  541. }
  542. settings.onError.call(context, errorMessage, $module, xhr);
  543. }
  544. }
  545. if(settings.errorDuration && status !== 'aborted') {
  546. module.debug('Adding error state');
  547. module.set.error();
  548. if( module.should.removeError() ) {
  549. setTimeout(module.remove.error, settings.errorDuration);
  550. }
  551. }
  552. module.debug('API Request failed', errorMessage, xhr);
  553. settings.onFailure.call(context, response, $module, xhr);
  554. }
  555. }
  556. },
  557. create: {
  558. request: function() {
  559. // api request promise
  560. return $.Deferred()
  561. .always(module.event.request.complete)
  562. .done(module.event.request.done)
  563. .fail(module.event.request.fail)
  564. ;
  565. },
  566. mockedXHR: function () {
  567. var
  568. // xhr does not simulate these properties of xhr but must return them
  569. textStatus = false,
  570. status = false,
  571. httpMessage = false,
  572. responder = settings.mockResponse || settings.response,
  573. asyncResponder = settings.mockResponseAsync || settings.responseAsync,
  574. asyncCallback,
  575. response,
  576. mockedXHR
  577. ;
  578. mockedXHR = $.Deferred()
  579. .always(module.event.xhr.complete)
  580. .done(module.event.xhr.done)
  581. .fail(module.event.xhr.fail)
  582. ;
  583. if(responder) {
  584. if( $.isFunction(responder) ) {
  585. module.debug('Using specified synchronous callback', responder);
  586. response = responder.call(context, requestSettings);
  587. }
  588. else {
  589. module.debug('Using settings specified response', responder);
  590. response = responder;
  591. }
  592. // simulating response
  593. mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
  594. }
  595. else if( $.isFunction(asyncResponder) ) {
  596. asyncCallback = function(response) {
  597. module.debug('Async callback returned response', response);
  598. if(response) {
  599. mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
  600. }
  601. else {
  602. mockedXHR.rejectWith(context, [{ responseText: response }, status, httpMessage]);
  603. }
  604. };
  605. module.debug('Using specified async response callback', asyncResponder);
  606. asyncResponder.call(context, requestSettings, asyncCallback);
  607. }
  608. return mockedXHR;
  609. },
  610. xhr: function() {
  611. var
  612. xhr
  613. ;
  614. // ajax request promise
  615. xhr = $.ajax(ajaxSettings)
  616. .always(module.event.xhr.always)
  617. .done(module.event.xhr.done)
  618. .fail(module.event.xhr.fail)
  619. ;
  620. module.verbose('Created server request', xhr, ajaxSettings);
  621. return xhr;
  622. }
  623. },
  624. set: {
  625. error: function() {
  626. module.verbose('Adding error state to element', $context);
  627. $context.addClass(className.error);
  628. },
  629. loading: function() {
  630. module.verbose('Adding loading state to element', $context);
  631. $context.addClass(className.loading);
  632. requestStartTime = new Date().getTime();
  633. }
  634. },
  635. remove: {
  636. error: function() {
  637. module.verbose('Removing error state from element', $context);
  638. $context.removeClass(className.error);
  639. },
  640. loading: function() {
  641. module.verbose('Removing loading state from element', $context);
  642. $context.removeClass(className.loading);
  643. }
  644. },
  645. get: {
  646. responseFromXHR: function(xhr) {
  647. return $.isPlainObject(xhr)
  648. ? (module.is.expectingJSON())
  649. ? module.decode.json(xhr.responseText)
  650. : xhr.responseText
  651. : false
  652. ;
  653. },
  654. errorFromRequest: function(response, status, httpMessage) {
  655. return ($.isPlainObject(response) && response.error !== undefined)
  656. ? response.error // use json error message
  657. : (settings.error[status] !== undefined) // use server error message
  658. ? settings.error[status]
  659. : httpMessage
  660. ;
  661. },
  662. request: function() {
  663. return module.request || false;
  664. },
  665. xhr: function() {
  666. return module.xhr || false;
  667. },
  668. settings: function() {
  669. var
  670. runSettings
  671. ;
  672. runSettings = settings.beforeSend.call($module, settings);
  673. if(runSettings) {
  674. if(runSettings.success !== undefined) {
  675. module.debug('Legacy success callback detected', runSettings);
  676. module.error(error.legacyParameters, runSettings.success);
  677. runSettings.onSuccess = runSettings.success;
  678. }
  679. if(runSettings.failure !== undefined) {
  680. module.debug('Legacy failure callback detected', runSettings);
  681. module.error(error.legacyParameters, runSettings.failure);
  682. runSettings.onFailure = runSettings.failure;
  683. }
  684. if(runSettings.complete !== undefined) {
  685. module.debug('Legacy complete callback detected', runSettings);
  686. module.error(error.legacyParameters, runSettings.complete);
  687. runSettings.onComplete = runSettings.complete;
  688. }
  689. }
  690. if(runSettings === undefined) {
  691. module.error(error.noReturnedValue);
  692. }
  693. if(runSettings === false) {
  694. return runSettings;
  695. }
  696. return (runSettings !== undefined)
  697. ? $.extend(true, {}, runSettings)
  698. : $.extend(true, {}, settings)
  699. ;
  700. },
  701. urlEncodedValue: function(value) {
  702. var
  703. decodedValue = window.decodeURIComponent(value),
  704. encodedValue = window.encodeURIComponent(value),
  705. alreadyEncoded = (decodedValue !== value)
  706. ;
  707. if(alreadyEncoded) {
  708. module.debug('URL value is already encoded, avoiding double encoding', value);
  709. return value;
  710. }
  711. module.verbose('Encoding value using encodeURIComponent', value, encodedValue);
  712. return encodedValue;
  713. },
  714. defaultData: function() {
  715. var
  716. data = {}
  717. ;
  718. if( !$.isWindow(element) ) {
  719. if( module.is.input() ) {
  720. data.value = $module.val();
  721. }
  722. else if( module.is.form() ) {
  723. }
  724. else {
  725. data.text = $module.text();
  726. }
  727. }
  728. return data;
  729. },
  730. event: function() {
  731. if( $.isWindow(element) || settings.on == 'now' ) {
  732. module.debug('API called without element, no events attached');
  733. return false;
  734. }
  735. else if(settings.on == 'auto') {
  736. if( $module.is('input') ) {
  737. return (element.oninput !== undefined)
  738. ? 'input'
  739. : (element.onpropertychange !== undefined)
  740. ? 'propertychange'
  741. : 'keyup'
  742. ;
  743. }
  744. else if( $module.is('form') ) {
  745. return 'submit';
  746. }
  747. else {
  748. return 'click';
  749. }
  750. }
  751. else {
  752. return settings.on;
  753. }
  754. },
  755. templatedURL: function(action) {
  756. action = action || $module.data(metadata.action) || settings.action || false;
  757. url = $module.data(metadata.url) || settings.url || false;
  758. if(url) {
  759. module.debug('Using specified url', url);
  760. return url;
  761. }
  762. if(action) {
  763. module.debug('Looking up url for action', action, settings.api);
  764. if(settings.api[action] === undefined && !module.is.mocked()) {
  765. module.error(error.missingAction, settings.action, settings.api);
  766. return;
  767. }
  768. url = settings.api[action];
  769. }
  770. else if( module.is.form() ) {
  771. url = $module.attr('action') || $context.attr('action') || false;
  772. module.debug('No url or action specified, defaulting to form action', url);
  773. }
  774. return url;
  775. }
  776. },
  777. abort: function() {
  778. var
  779. xhr = module.get.xhr()
  780. ;
  781. if( xhr && xhr.state() !== 'resolved') {
  782. module.debug('Cancelling API request');
  783. xhr.abort();
  784. }
  785. },
  786. // reset state
  787. reset: function() {
  788. module.remove.error();
  789. module.remove.loading();
  790. },
  791. setting: function(name, value) {
  792. module.debug('Changing setting', name, value);
  793. if( $.isPlainObject(name) ) {
  794. $.extend(true, settings, name);
  795. }
  796. else if(value !== undefined) {
  797. if($.isPlainObject(settings[name])) {
  798. $.extend(true, settings[name], value);
  799. }
  800. else {
  801. settings[name] = value;
  802. }
  803. }
  804. else {
  805. return settings[name];
  806. }
  807. },
  808. internal: function(name, value) {
  809. if( $.isPlainObject(name) ) {
  810. $.extend(true, module, name);
  811. }
  812. else if(value !== undefined) {
  813. module[name] = value;
  814. }
  815. else {
  816. return module[name];
  817. }
  818. },
  819. debug: function() {
  820. if(!settings.silent && settings.debug) {
  821. if(settings.performance) {
  822. module.performance.log(arguments);
  823. }
  824. else {
  825. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  826. module.debug.apply(console, arguments);
  827. }
  828. }
  829. },
  830. verbose: function() {
  831. if(!settings.silent && settings.verbose && settings.debug) {
  832. if(settings.performance) {
  833. module.performance.log(arguments);
  834. }
  835. else {
  836. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  837. module.verbose.apply(console, arguments);
  838. }
  839. }
  840. },
  841. error: function() {
  842. if(!settings.silent) {
  843. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  844. module.error.apply(console, arguments);
  845. }
  846. },
  847. performance: {
  848. log: function(message) {
  849. var
  850. currentTime,
  851. executionTime,
  852. previousTime
  853. ;
  854. if(settings.performance) {
  855. currentTime = new Date().getTime();
  856. previousTime = time || currentTime;
  857. executionTime = currentTime - previousTime;
  858. time = currentTime;
  859. performance.push({
  860. 'Name' : message[0],
  861. 'Arguments' : [].slice.call(message, 1) || '',
  862. //'Element' : element,
  863. 'Execution Time' : executionTime
  864. });
  865. }
  866. clearTimeout(module.performance.timer);
  867. module.performance.timer = setTimeout(module.performance.display, 500);
  868. },
  869. display: function() {
  870. var
  871. title = settings.name + ':',
  872. totalTime = 0
  873. ;
  874. time = false;
  875. clearTimeout(module.performance.timer);
  876. $.each(performance, function(index, data) {
  877. totalTime += data['Execution Time'];
  878. });
  879. title += ' ' + totalTime + 'ms';
  880. if(moduleSelector) {
  881. title += ' \'' + moduleSelector + '\'';
  882. }
  883. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  884. console.groupCollapsed(title);
  885. if(console.table) {
  886. console.table(performance);
  887. }
  888. else {
  889. $.each(performance, function(index, data) {
  890. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  891. });
  892. }
  893. console.groupEnd();
  894. }
  895. performance = [];
  896. }
  897. },
  898. invoke: function(query, passedArguments, context) {
  899. var
  900. object = instance,
  901. maxDepth,
  902. found,
  903. response
  904. ;
  905. passedArguments = passedArguments || queryArguments;
  906. context = element || context;
  907. if(typeof query == 'string' && object !== undefined) {
  908. query = query.split(/[\. ]/);
  909. maxDepth = query.length - 1;
  910. $.each(query, function(depth, value) {
  911. var camelCaseValue = (depth != maxDepth)
  912. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  913. : query
  914. ;
  915. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  916. object = object[camelCaseValue];
  917. }
  918. else if( object[camelCaseValue] !== undefined ) {
  919. found = object[camelCaseValue];
  920. return false;
  921. }
  922. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  923. object = object[value];
  924. }
  925. else if( object[value] !== undefined ) {
  926. found = object[value];
  927. return false;
  928. }
  929. else {
  930. module.error(error.method, query);
  931. return false;
  932. }
  933. });
  934. }
  935. if ( $.isFunction( found ) ) {
  936. response = found.apply(context, passedArguments);
  937. }
  938. else if(found !== undefined) {
  939. response = found;
  940. }
  941. if(Array.isArray(returnedValue)) {
  942. returnedValue.push(response);
  943. }
  944. else if(returnedValue !== undefined) {
  945. returnedValue = [returnedValue, response];
  946. }
  947. else if(response !== undefined) {
  948. returnedValue = response;
  949. }
  950. return found;
  951. }
  952. };
  953. if(methodInvoked) {
  954. if(instance === undefined) {
  955. module.initialize();
  956. }
  957. module.invoke(query);
  958. }
  959. else {
  960. if(instance !== undefined) {
  961. instance.invoke('destroy');
  962. }
  963. module.initialize();
  964. }
  965. })
  966. ;
  967. return (returnedValue !== undefined)
  968. ? returnedValue
  969. : this
  970. ;
  971. };
  972. $.api.settings = {
  973. name : 'API',
  974. namespace : 'api',
  975. debug : false,
  976. verbose : false,
  977. performance : true,
  978. // object containing all templates endpoints
  979. api : {},
  980. // whether to cache responses
  981. cache : true,
  982. // whether new requests should abort previous requests
  983. interruptRequests : true,
  984. // event binding
  985. on : 'auto',
  986. // context for applying state classes
  987. stateContext : false,
  988. // duration for loading state
  989. loadingDuration : 0,
  990. // whether to hide errors after a period of time
  991. hideError : 'auto',
  992. // duration for error state
  993. errorDuration : 2000,
  994. // whether parameters should be encoded with encodeURIComponent
  995. encodeParameters : true,
  996. // API action to use
  997. action : false,
  998. // templated URL to use
  999. url : false,
  1000. // base URL to apply to all endpoints
  1001. base : '',
  1002. // data that will
  1003. urlData : {},
  1004. // whether to add default data to url data
  1005. defaultData : true,
  1006. // whether to serialize closest form
  1007. serializeForm : false,
  1008. // how long to wait before request should occur
  1009. throttle : 0,
  1010. // whether to throttle first request or only repeated
  1011. throttleFirstRequest : true,
  1012. // standard ajax settings
  1013. method : 'get',
  1014. data : {},
  1015. dataType : 'json',
  1016. // mock response
  1017. mockResponse : false,
  1018. mockResponseAsync : false,
  1019. // aliases for mock
  1020. response : false,
  1021. responseAsync : false,
  1022. // whether onResponse should work with response value without force converting into an object
  1023. rawResponse : false,
  1024. // callbacks before request
  1025. beforeSend : function(settings) { return settings; },
  1026. beforeXHR : function(xhr) {},
  1027. onRequest : function(promise, xhr) {},
  1028. // after request
  1029. onResponse : false, // function(response) { },
  1030. // response was successful, if JSON passed validation
  1031. onSuccess : function(response, $module) {},
  1032. // request finished without aborting
  1033. onComplete : function(response, $module) {},
  1034. // failed JSON success test
  1035. onFailure : function(response, $module) {},
  1036. // server error
  1037. onError : function(errorMessage, $module) {},
  1038. // request aborted
  1039. onAbort : function(errorMessage, $module) {},
  1040. successTest : false,
  1041. // errors
  1042. error : {
  1043. beforeSend : 'The before send function has aborted the request',
  1044. error : 'There was an error with your request',
  1045. exitConditions : 'API Request Aborted. Exit conditions met',
  1046. JSONParse : 'JSON could not be parsed during error handling',
  1047. legacyParameters : 'You are using legacy API success callback names',
  1048. method : 'The method you called is not defined',
  1049. missingAction : 'API action used but no url was defined',
  1050. missingSerialize : 'jquery-serialize-object is required to add form data to an existing data object',
  1051. missingURL : 'No URL specified for api event',
  1052. noReturnedValue : 'The beforeSend callback must return a settings object, beforeSend ignored.',
  1053. noStorage : 'Caching responses locally requires session storage',
  1054. parseError : 'There was an error parsing your request',
  1055. requiredParameter : 'Missing a required URL parameter: ',
  1056. statusMessage : 'Server gave an error: ',
  1057. timeout : 'Your request timed out'
  1058. },
  1059. regExp : {
  1060. required : /\{\$*[A-z0-9]+\}/g,
  1061. optional : /\{\/\$*[A-z0-9]+\}/g,
  1062. },
  1063. className: {
  1064. loading : 'loading',
  1065. error : 'error'
  1066. },
  1067. selector: {
  1068. disabled : '.disabled',
  1069. form : 'form'
  1070. },
  1071. metadata: {
  1072. action : 'action',
  1073. url : 'url'
  1074. }
  1075. };
  1076. })( jQuery, window, document );