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.

839 lines
27 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
  1. /*
  2. * # Semantic - API
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 Contributor
  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 = ( $.isPlainObject(parameters) )
  30. ? $.extend(true, {}, $.fn.api.settings, parameters)
  31. : $.extend({}, $.fn.api.settings),
  32. // internal aliases
  33. namespace = settings.namespace,
  34. metadata = settings.metadata,
  35. selector = settings.selector,
  36. error = settings.error,
  37. className = settings.className,
  38. // define namespaces for modules
  39. eventNamespace = '.' + namespace,
  40. moduleNamespace = 'module-' + namespace,
  41. // element that creates request
  42. $module = $(this),
  43. $form = $module.closest(selector.form),
  44. // context used for state
  45. $context = (settings.stateContext)
  46. ? $(settings.stateContext)
  47. : $module,
  48. // request details
  49. ajaxSettings,
  50. requestSettings,
  51. url,
  52. data,
  53. // standard module
  54. element = this,
  55. context = $context.get(),
  56. instance = $module.data(moduleNamespace),
  57. module
  58. ;
  59. module = {
  60. initialize: function() {
  61. var
  62. triggerEvent = module.get.event()
  63. ;
  64. // bind events
  65. if(!methodInvoked) {
  66. if( triggerEvent ) {
  67. module.debug('Attaching API events to element', triggerEvent);
  68. $module
  69. .on(triggerEvent + eventNamespace, module.event.trigger)
  70. ;
  71. }
  72. else {
  73. module.query();
  74. }
  75. }
  76. module.instantiate();
  77. },
  78. instantiate: function() {
  79. module.verbose('Storing instance of module', module);
  80. instance = module;
  81. $module
  82. .data(moduleNamespace, instance)
  83. ;
  84. },
  85. destroy: function() {
  86. module.verbose('Destroying previous module for', element);
  87. $module
  88. .removeData(moduleNamespace)
  89. .off(eventNamespace)
  90. ;
  91. },
  92. query: function() {
  93. if(module.is.disabled()) {
  94. module.debug('Element is disabled API request aborted');
  95. return;
  96. }
  97. // determine if an api event already occurred
  98. if(module.is.loading() && settings.throttle === 0 ) {
  99. module.debug('Cancelling request, previous request is still pending');
  100. return;
  101. }
  102. // pass element metadata to url (value, text)
  103. if(settings.defaultData) {
  104. $.extend(true, settings.urlData, module.get.defaultData());
  105. }
  106. // Add form content
  107. if(settings.serializeForm !== false || $context.is('form')) {
  108. if(settings.serializeForm == 'json') {
  109. $.extend(true, settings.data, module.get.formData());
  110. }
  111. else {
  112. settings.data = module.get.formData();
  113. }
  114. }
  115. // call beforesend and get any settings changes
  116. requestSettings = module.get.settings();
  117. // check if beforesend cancelled request
  118. if(requestSettings === false) {
  119. module.error(error.beforeSend);
  120. return;
  121. }
  122. if(settings.url) {
  123. // override with url if specified
  124. module.debug('Using specified url', url);
  125. url = module.add.urlData( settings.url );
  126. }
  127. else {
  128. // otherwise find url from api endpoints
  129. url = module.add.urlData( module.get.templateURL() );
  130. module.debug('Added URL Data to url', url);
  131. }
  132. // exit conditions reached, missing url parameters
  133. if( !url ) {
  134. if($module.is('form')) {
  135. module.debug('No url or action specified, defaulting to form action');
  136. url = $module.attr('action');
  137. }
  138. else {
  139. module.error(error.missingURL, settings.action);
  140. return;
  141. }
  142. }
  143. // add loading state
  144. module.set.loading();
  145. // look for jQuery ajax parameters in settings
  146. ajaxSettings = $.extend(true, {}, settings, {
  147. type : settings.method || settings.type,
  148. data : data,
  149. url : settings.base + url,
  150. beforeSend : settings.beforeXHR,
  151. success : function() {},
  152. failure : function() {},
  153. complete : function() {}
  154. });
  155. module.verbose('Creating AJAX request with settings', ajaxSettings);
  156. if( !module.is.loading() ) {
  157. module.request = module.create.request();
  158. module.xhr = module.create.xhr();
  159. }
  160. else {
  161. // throttle additional requests
  162. module.timer = setTimeout(function() {
  163. module.request = module.create.request();
  164. module.xhr = module.create.xhr();
  165. }, settings.throttle);
  166. }
  167. },
  168. is: {
  169. disabled: function() {
  170. return ($module.filter(settings.filter).size() > 0);
  171. },
  172. loading: function() {
  173. return (module.request && module.request.state() == 'pending');
  174. }
  175. },
  176. was: {
  177. succesful: function() {
  178. return (module.request && module.request.state() == 'resolved');
  179. },
  180. failure: function() {
  181. return (module.request && module.request.state() == 'rejected');
  182. },
  183. complete: function() {
  184. return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
  185. }
  186. },
  187. add: {
  188. urlData: function(url, urlData) {
  189. var
  190. requiredVariables,
  191. optionalVariables
  192. ;
  193. if(url) {
  194. requiredVariables = url.match(settings.regExp.required);
  195. optionalVariables = url.match(settings.regExp.optional);
  196. urlData = urlData || settings.urlData;
  197. if(requiredVariables) {
  198. module.debug('Looking for required URL variables', requiredVariables);
  199. $.each(requiredVariables, function(index, templatedString) {
  200. var
  201. // allow legacy {$var} style
  202. variable = (templatedString.indexOf('$') !== -1)
  203. ? templatedString.substr(2, templatedString.length - 3)
  204. : templatedString.substr(1, templatedString.length - 2),
  205. value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
  206. ? urlData[variable]
  207. : ($module.data(variable) !== undefined)
  208. ? $module.data(variable)
  209. : ($context.data(variable) !== undefined)
  210. ? $context.data(variable)
  211. : urlData[variable]
  212. ;
  213. // remove value
  214. if(value === undefined) {
  215. module.error(error.requiredParameter, variable, url);
  216. url = false;
  217. return false;
  218. }
  219. else {
  220. module.verbose('Found required variable', variable, value);
  221. url = url.replace(templatedString, value);
  222. }
  223. });
  224. }
  225. if(optionalVariables) {
  226. module.debug('Looking for optional URL variables', requiredVariables);
  227. $.each(optionalVariables, function(index, templatedString) {
  228. var
  229. // allow legacy {/$var} style
  230. variable = (templatedString.indexOf('$') !== -1)
  231. ? templatedString.substr(3, templatedString.length - 4)
  232. : templatedString.substr(2, templatedString.length - 3),
  233. value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
  234. ? urlData[variable]
  235. : ($module.data(variable) !== undefined)
  236. ? $module.data(variable)
  237. : ($context.data(variable) !== undefined)
  238. ? $context.data(variable)
  239. : urlData[variable]
  240. ;
  241. // optional replacement
  242. if(value !== undefined) {
  243. module.verbose('Optional variable Found', variable, value);
  244. url = url.replace(templatedString, value);
  245. }
  246. else {
  247. module.verbose('Optional variable not found', variable);
  248. // remove preceding slash if set
  249. if(url.indexOf('/' + templatedString) !== -1) {
  250. url = url.replace('/' + templatedString, '');
  251. }
  252. else {
  253. url = url.replace(templatedString, '');
  254. }
  255. }
  256. });
  257. }
  258. }
  259. return url;
  260. }
  261. },
  262. event: {
  263. trigger: function(event) {
  264. module.query();
  265. if(event.type == 'submit' || event.type == 'click') {
  266. event.preventDefault();
  267. }
  268. },
  269. xhr: {
  270. always: function() {
  271. // calculate if loading time was below minimum threshold
  272. },
  273. done: function(response) {
  274. var
  275. context = this,
  276. elapsedTime = (new Date().getTime() - time),
  277. timeLeft = (settings.loadingDuration - elapsedTime)
  278. ;
  279. timeLeft = (timeLeft > 0)
  280. ? timeLeft
  281. : 0
  282. ;
  283. setTimeout(function() {
  284. module.request.resolveWith(context, [response]);
  285. }, timeLeft);
  286. },
  287. fail: function(xhr, status, httpMessage) {
  288. var
  289. context = this,
  290. elapsedTime = (new Date().getTime() - time),
  291. timeLeft = (settings.loadingDuration - elapsedTime)
  292. ;
  293. timeLeft = (timeLeft > 0)
  294. ? timeLeft
  295. : 0
  296. ;
  297. // page triggers abort on navigation, dont show error
  298. setTimeout(function() {
  299. if(status !== 'abort') {
  300. module.request.rejectWith(context, [xhr, status, httpMessage]);
  301. }
  302. else {
  303. module.reset();
  304. }
  305. }, timeLeft);
  306. }
  307. },
  308. request: {
  309. complete: function(response) {
  310. module.remove.loading();
  311. $.proxy(settings.onComplete, context)(response, $module);
  312. },
  313. done: function(response) {
  314. module.debug('API Response Received', response);
  315. if(settings.dataType == 'json') {
  316. if( $.isFunction(settings.successTest) ) {
  317. module.debug('Checking JSON returned success', settings.successTest, response);
  318. if( settings.successTest(response) ) {
  319. $.proxy(settings.onSuccess, context)(response, $module);
  320. }
  321. else {
  322. module.debug('JSON test specified by user and response failed', response);
  323. $.proxy(settings.onFailure, context)(response, $module);
  324. }
  325. }
  326. else {
  327. $.proxy(settings.onSuccess, context)(response, $module);
  328. }
  329. }
  330. else {
  331. $.proxy(settings.onSuccess, context)(response, $module);
  332. }
  333. },
  334. error: function(xhr, status, httpMessage) {
  335. var
  336. errorMessage = (settings.error[status] !== undefined)
  337. ? settings.error[status]
  338. : httpMessage,
  339. response
  340. ;
  341. // let em know unless request aborted
  342. if(xhr !== undefined) {
  343. // readyState 4 = done, anything less is not really sent
  344. if(xhr.readyState !== undefined && xhr.readyState == 4) {
  345. // if http status code returned and json returned error, look for it
  346. if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
  347. module.error(error.statusMessage + httpMessage);
  348. }
  349. else {
  350. if(status == 'error' && settings.dataType == 'json') {
  351. try {
  352. response = $.parseJSON(xhr.responseText);
  353. if(response && response.error !== undefined) {
  354. errorMessage = response.error;
  355. }
  356. }
  357. catch(e) {
  358. module.error(error.JSONParse);
  359. }
  360. }
  361. }
  362. module.remove.loading();
  363. module.set.error();
  364. // show error state only for duration specified in settings
  365. if(settings.errorDuration) {
  366. setTimeout(module.remove.error, settings.errorDuration);
  367. }
  368. module.debug('API Request error:', errorMessage);
  369. $.proxy(settings.onError, context)(errorMessage, context);
  370. }
  371. else {
  372. $.proxy(settings.onAbort, context)(errorMessage, context);
  373. module.debug('Request Aborted (Most likely caused by page change or CORS Policy)', status, httpMessage);
  374. }
  375. }
  376. }
  377. }
  378. },
  379. create: {
  380. request: function() {
  381. return $.Deferred()
  382. .always(module.event.request.complete)
  383. .done(module.event.request.done)
  384. .fail(module.event.request.error)
  385. ;
  386. },
  387. xhr: function() {
  388. $.ajax(ajaxSettings)
  389. .always(module.event.xhr.always)
  390. .done(module.event.xhr.done)
  391. .fail(module.event.xhr.fail)
  392. ;
  393. }
  394. },
  395. set: {
  396. error: function() {
  397. module.verbose('Adding error state to element', $context);
  398. $context.addClass(className.error);
  399. },
  400. loading: function() {
  401. module.verbose('Adding loading state to element', $context);
  402. $context.addClass(className.loading);
  403. }
  404. },
  405. remove: {
  406. error: function() {
  407. module.verbose('Removing error state from element', $context);
  408. $context.removeClass(className.error);
  409. },
  410. loading: function() {
  411. module.verbose('Removing loading state from element', $context);
  412. $context.removeClass(className.loading);
  413. }
  414. },
  415. get: {
  416. request: function() {
  417. return module.request || false;
  418. },
  419. xhr: function() {
  420. return module.xhr || false;
  421. },
  422. settings: function() {
  423. var
  424. runSettings
  425. ;
  426. runSettings = $.proxy(settings.beforeSend, $module)(settings);
  427. if(runSettings) {
  428. if(runSettings.success !== undefined) {
  429. module.debug('Legacy success callback detected', runSettings);
  430. module.error(error.legacyParameters, runSettings.success);
  431. runSettings.onSuccess = runSettings.success;
  432. }
  433. if(runSettings.failure !== undefined) {
  434. module.debug('Legacy failure callback detected', runSettings);
  435. module.error(error.legacyParameters, runSettings.failure);
  436. runSettings.onFailure = runSettings.failure;
  437. }
  438. if(runSettings.complete !== undefined) {
  439. module.debug('Legacy complete callback detected', runSettings);
  440. module.error(error.legacyParameters, runSettings.complete);
  441. runSettings.onComplete = runSettings.complete;
  442. }
  443. }
  444. if(runSettings === undefined) {
  445. module.error(error.noReturnedValue);
  446. }
  447. return (runSettings !== undefined)
  448. ? runSettings
  449. : settings
  450. ;
  451. },
  452. defaultData: function() {
  453. var
  454. data = {}
  455. ;
  456. if( !$.isWindow(element) ) {
  457. if( $module.is('input') ) {
  458. data.value = $module.val();
  459. }
  460. else if( $module.is('form') ) {
  461. }
  462. else {
  463. data.text = $module.text();
  464. }
  465. }
  466. return data;
  467. },
  468. event: function() {
  469. if( $.isWindow(element) || settings.on == 'now' ) {
  470. module.debug('API called without element, no events attached');
  471. return false;
  472. }
  473. else if(settings.on == 'auto') {
  474. if( $module.is('input') ) {
  475. return (element.oninput !== undefined)
  476. ? 'input'
  477. : (element.onpropertychange !== undefined)
  478. ? 'propertychange'
  479. : 'keyup'
  480. ;
  481. }
  482. else if( $module.is('form') ) {
  483. return 'submit';
  484. }
  485. else {
  486. return 'click';
  487. }
  488. }
  489. else {
  490. return settings.on;
  491. }
  492. },
  493. formData: function() {
  494. var
  495. formData
  496. ;
  497. if($(this).serializeObject() !== undefined) {
  498. formData = $form.serializeObject();
  499. }
  500. else {
  501. module.error(error.missingSerialize);
  502. formData = $form.serialize();
  503. }
  504. module.debug('Retrieved form data', formData);
  505. return formData;
  506. },
  507. templateURL: function(action) {
  508. var
  509. url
  510. ;
  511. action = action || $module.data(settings.metadata.action) || settings.action || false;
  512. if(action) {
  513. module.debug('Looking up url for action', action, settings.api);
  514. if(settings.api[action] !== undefined) {
  515. url = settings.api[action];
  516. module.debug('Found template url', url);
  517. }
  518. else {
  519. module.error(error.missingAction, settings.action, settings.api);
  520. }
  521. }
  522. return url;
  523. }
  524. },
  525. // reset state
  526. reset: function() {
  527. module.remove.error();
  528. module.remove.loading();
  529. },
  530. setting: function(name, value) {
  531. module.debug('Changing setting', name, value);
  532. if( $.isPlainObject(name) ) {
  533. $.extend(true, settings, name);
  534. }
  535. else if(value !== undefined) {
  536. settings[name] = value;
  537. }
  538. else {
  539. return settings[name];
  540. }
  541. },
  542. internal: function(name, value) {
  543. if( $.isPlainObject(name) ) {
  544. $.extend(true, module, name);
  545. }
  546. else if(value !== undefined) {
  547. module[name] = value;
  548. }
  549. else {
  550. return module[name];
  551. }
  552. },
  553. debug: function() {
  554. if(settings.debug) {
  555. if(settings.performance) {
  556. module.performance.log(arguments);
  557. }
  558. else {
  559. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  560. module.debug.apply(console, arguments);
  561. }
  562. }
  563. },
  564. verbose: function() {
  565. if(settings.verbose && settings.debug) {
  566. if(settings.performance) {
  567. module.performance.log(arguments);
  568. }
  569. else {
  570. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  571. module.verbose.apply(console, arguments);
  572. }
  573. }
  574. },
  575. error: function() {
  576. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  577. module.error.apply(console, arguments);
  578. },
  579. performance: {
  580. log: function(message) {
  581. var
  582. currentTime,
  583. executionTime,
  584. previousTime
  585. ;
  586. if(settings.performance) {
  587. currentTime = new Date().getTime();
  588. previousTime = time || currentTime;
  589. executionTime = currentTime - previousTime;
  590. time = currentTime;
  591. performance.push({
  592. 'Name' : message[0],
  593. 'Arguments' : [].slice.call(message, 1) || '',
  594. //'Element' : element,
  595. 'Execution Time' : executionTime
  596. });
  597. }
  598. clearTimeout(module.performance.timer);
  599. module.performance.timer = setTimeout(module.performance.display, 100);
  600. },
  601. display: function() {
  602. var
  603. title = settings.name + ':',
  604. totalTime = 0
  605. ;
  606. time = false;
  607. clearTimeout(module.performance.timer);
  608. $.each(performance, function(index, data) {
  609. totalTime += data['Execution Time'];
  610. });
  611. title += ' ' + totalTime + 'ms';
  612. if(moduleSelector) {
  613. title += ' \'' + moduleSelector + '\'';
  614. }
  615. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  616. console.groupCollapsed(title);
  617. if(console.table) {
  618. console.table(performance);
  619. }
  620. else {
  621. $.each(performance, function(index, data) {
  622. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  623. });
  624. }
  625. console.groupEnd();
  626. }
  627. performance = [];
  628. }
  629. },
  630. invoke: function(query, passedArguments, context) {
  631. var
  632. object = instance,
  633. maxDepth,
  634. found,
  635. response
  636. ;
  637. passedArguments = passedArguments || queryArguments;
  638. context = element || context;
  639. if(typeof query == 'string' && object !== undefined) {
  640. query = query.split(/[\. ]/);
  641. maxDepth = query.length - 1;
  642. $.each(query, function(depth, value) {
  643. var camelCaseValue = (depth != maxDepth)
  644. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  645. : query
  646. ;
  647. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  648. object = object[camelCaseValue];
  649. }
  650. else if( object[camelCaseValue] !== undefined ) {
  651. found = object[camelCaseValue];
  652. return false;
  653. }
  654. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  655. object = object[value];
  656. }
  657. else if( object[value] !== undefined ) {
  658. found = object[value];
  659. return false;
  660. }
  661. else {
  662. module.error(error.method, query);
  663. return false;
  664. }
  665. });
  666. }
  667. if ( $.isFunction( found ) ) {
  668. response = found.apply(context, passedArguments);
  669. }
  670. else if(found !== undefined) {
  671. response = found;
  672. }
  673. if($.isArray(returnedValue)) {
  674. returnedValue.push(response);
  675. }
  676. else if(returnedValue !== undefined) {
  677. returnedValue = [returnedValue, response];
  678. }
  679. else if(response !== undefined) {
  680. returnedValue = response;
  681. }
  682. return found;
  683. }
  684. };
  685. if(methodInvoked) {
  686. if(instance === undefined) {
  687. module.initialize();
  688. }
  689. module.invoke(query);
  690. }
  691. else {
  692. if(instance !== undefined) {
  693. module.destroy();
  694. }
  695. module.initialize();
  696. }
  697. })
  698. ;
  699. return (returnedValue !== undefined)
  700. ? returnedValue
  701. : this
  702. ;
  703. };
  704. $.api.settings = {
  705. name : 'API',
  706. namespace : 'api',
  707. debug : true,
  708. verbose : true,
  709. performance : true,
  710. // event binding
  711. on : 'auto',
  712. filter : '.disabled',
  713. stateContext : false,
  714. // state
  715. loadingDuration : 0,
  716. errorDuration : 2000,
  717. // templating
  718. action : false,
  719. url : false,
  720. base : '',
  721. // data
  722. urlData : {},
  723. // ui
  724. defaultData : true,
  725. serializeForm : false,
  726. throttle : 0,
  727. // jQ ajax
  728. method : 'get',
  729. data : {},
  730. dataType : 'json',
  731. // callbacks
  732. beforeSend : function(settings) { return settings; },
  733. beforeXHR : function(xhr) {},
  734. onSuccess : function(response, $module) {},
  735. onComplete : function(response, $module) {},
  736. onFailure : function(errorMessage, $module) {},
  737. onError : function(errorMessage, $module) {},
  738. onAbort : function(errorMessage, $module) {},
  739. successTest : false,
  740. // errors
  741. error : {
  742. beforeSend : 'The before send function has aborted the request',
  743. error : 'There was an error with your request',
  744. exitConditions : 'API Request Aborted. Exit conditions met',
  745. JSONParse : 'JSON could not be parsed during error handling',
  746. legacyParameters : 'You are using legacy API success callback names',
  747. method : 'The method you called is not defined',
  748. missingAction : 'API action used but no url was defined',
  749. missingSerialize : 'Required dependency jquery-serialize-object missing, using basic serialize',
  750. missingURL : 'No URL specified for api event',
  751. noReturnedValue : 'The beforeSend callback must return a settings object, beforeSend ignored.',
  752. parseError : 'There was an error parsing your request',
  753. requiredParameter : 'Missing a required URL parameter: ',
  754. statusMessage : 'Server gave an error: ',
  755. timeout : 'Your request timed out'
  756. },
  757. regExp : {
  758. required: /\{\$*[A-z0-9]+\}/g,
  759. optional: /\{\/\$*[A-z0-9]+\}/g,
  760. },
  761. className: {
  762. loading : 'loading',
  763. error : 'error'
  764. },
  765. selector: {
  766. form: 'form'
  767. },
  768. metadata: {
  769. action : 'action',
  770. request : 'request',
  771. xhr : 'xhr'
  772. }
  773. };
  774. $.api.settings.api = {};
  775. })( jQuery, window , document );