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
10 years ago
10 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).length > 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 );