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.

784 lines
25 KiB

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