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.

818 lines
26 KiB

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