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.

752 lines
23 KiB

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