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