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.

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