api.js 39 KB

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