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.

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