Code Duplication    Length = 1157-1157 lines in 2 locations

public/lib/semantic/semantic.js 1 location

@@ 19706-20862 (lines=1157) @@
19703
 *
19704
 */
19705
19706
;(function ($, window, document, undefined) {
19707
19708
"use strict";
19709
19710
var
19711
  window = (typeof window != 'undefined' && window.Math == Math)
19712
    ? window
19713
    : (typeof self != 'undefined' && self.Math == Math)
19714
      ? self
19715
      : Function('return this')()
19716
;
19717
19718
$.api = $.fn.api = function(parameters) {
19719
19720
  var
19721
    // use window context if none specified
19722
    $allModules     = $.isFunction(this)
19723
        ? $(window)
19724
        : $(this),
19725
    moduleSelector = $allModules.selector || '',
19726
    time           = new Date().getTime(),
19727
    performance    = [],
19728
19729
    query          = arguments[0],
19730
    methodInvoked  = (typeof query == 'string'),
19731
    queryArguments = [].slice.call(arguments, 1),
19732
19733
    returnedValue
19734
  ;
19735
19736
  $allModules
19737
    .each(function() {
19738
      var
19739
        settings          = ( $.isPlainObject(parameters) )
19740
          ? $.extend(true, {}, $.fn.api.settings, parameters)
19741
          : $.extend({}, $.fn.api.settings),
19742
19743
        // internal aliases
19744
        namespace       = settings.namespace,
19745
        metadata        = settings.metadata,
19746
        selector        = settings.selector,
19747
        error           = settings.error,
19748
        className       = settings.className,
19749
19750
        // define namespaces for modules
19751
        eventNamespace  = '.' + namespace,
19752
        moduleNamespace = 'module-' + namespace,
19753
19754
        // element that creates request
19755
        $module         = $(this),
19756
        $form           = $module.closest(selector.form),
19757
19758
        // context used for state
19759
        $context        = (settings.stateContext)
19760
          ? $(settings.stateContext)
19761
          : $module,
19762
19763
        // request details
19764
        ajaxSettings,
19765
        requestSettings,
19766
        url,
19767
        data,
19768
        requestStartTime,
19769
19770
        // standard module
19771
        element         = this,
19772
        context         = $context[0],
19773
        instance        = $module.data(moduleNamespace),
19774
        module
19775
      ;
19776
19777
      module = {
19778
19779
        initialize: function() {
19780
          if(!methodInvoked) {
19781
            module.bind.events();
19782
          }
19783
          module.instantiate();
19784
        },
19785
19786
        instantiate: function() {
19787
          module.verbose('Storing instance of module', module);
19788
          instance = module;
19789
          $module
19790
            .data(moduleNamespace, instance)
19791
          ;
19792
        },
19793
19794
        destroy: function() {
19795
          module.verbose('Destroying previous module for', element);
19796
          $module
19797
            .removeData(moduleNamespace)
19798
            .off(eventNamespace)
19799
          ;
19800
        },
19801
19802
        bind: {
19803
          events: function() {
19804
            var
19805
              triggerEvent = module.get.event()
19806
            ;
19807
            if( triggerEvent ) {
19808
              module.verbose('Attaching API events to element', triggerEvent);
19809
              $module
19810
                .on(triggerEvent + eventNamespace, module.event.trigger)
19811
              ;
19812
            }
19813
            else if(settings.on == 'now') {
19814
              module.debug('Querying API endpoint immediately');
19815
              module.query();
19816
            }
19817
          }
19818
        },
19819
19820
        decode: {
19821
          json: function(response) {
19822
            if(response !== undefined && typeof response == 'string') {
19823
              try {
19824
               response = JSON.parse(response);
19825
              }
19826
              catch(e) {
19827
                // isnt json string
19828
              }
19829
            }
19830
            return response;
19831
          }
19832
        },
19833
19834
        read: {
19835
          cachedResponse: function(url) {
19836
            var
19837
              response
19838
            ;
19839
            if(window.Storage === undefined) {
19840
              module.error(error.noStorage);
19841
              return;
19842
            }
19843
            response = sessionStorage.getItem(url);
19844
            module.debug('Using cached response', url, response);
19845
            response = module.decode.json(response);
19846
            return response;
19847
          }
19848
        },
19849
        write: {
19850
          cachedResponse: function(url, response) {
19851
            if(response && response === '') {
19852
              module.debug('Response empty, not caching', response);
19853
              return;
19854
            }
19855
            if(window.Storage === undefined) {
19856
              module.error(error.noStorage);
19857
              return;
19858
            }
19859
            if( $.isPlainObject(response) ) {
19860
              response = JSON.stringify(response);
19861
            }
19862
            sessionStorage.setItem(url, response);
19863
            module.verbose('Storing cached response for url', url, response);
19864
          }
19865
        },
19866
19867
        query: function() {
19868
19869
          if(module.is.disabled()) {
19870
            module.debug('Element is disabled API request aborted');
19871
            return;
19872
          }
19873
19874
          if(module.is.loading()) {
19875
            if(settings.interruptRequests) {
19876
              module.debug('Interrupting previous request');
19877
              module.abort();
19878
            }
19879
            else {
19880
              module.debug('Cancelling request, previous request is still pending');
19881
              return;
19882
            }
19883
          }
19884
19885
          // pass element metadata to url (value, text)
19886
          if(settings.defaultData) {
19887
            $.extend(true, settings.urlData, module.get.defaultData());
19888
          }
19889
19890
          // Add form content
19891
          if(settings.serializeForm) {
19892
            settings.data = module.add.formData(settings.data);
19893
          }
19894
19895
          // call beforesend and get any settings changes
19896
          requestSettings = module.get.settings();
19897
19898
          // check if before send cancelled request
19899
          if(requestSettings === false) {
19900
            module.cancelled = true;
19901
            module.error(error.beforeSend);
19902
            return;
19903
          }
19904
          else {
19905
            module.cancelled = false;
19906
          }
19907
19908
          // get url
19909
          url = module.get.templatedURL();
19910
19911
          if(!url && !module.is.mocked()) {
19912
            module.error(error.missingURL);
19913
            return;
19914
          }
19915
19916
          // replace variables
19917
          url = module.add.urlData( url );
19918
          // missing url parameters
19919
          if( !url && !module.is.mocked()) {
19920
            return;
19921
          }
19922
19923
          requestSettings.url = settings.base + url;
19924
19925
          // look for jQuery ajax parameters in settings
19926
          ajaxSettings = $.extend(true, {}, settings, {
19927
            type       : settings.method || settings.type,
19928
            data       : data,
19929
            url        : settings.base + url,
19930
            beforeSend : settings.beforeXHR,
19931
            success    : function() {},
19932
            failure    : function() {},
19933
            complete   : function() {}
19934
          });
19935
19936
          module.debug('Querying URL', ajaxSettings.url);
19937
          module.verbose('Using AJAX settings', ajaxSettings);
19938
          if(settings.cache === 'local' && module.read.cachedResponse(url)) {
19939
            module.debug('Response returned from local cache');
19940
            module.request = module.create.request();
19941
            module.request.resolveWith(context, [ module.read.cachedResponse(url) ]);
19942
            return;
19943
          }
19944
19945
          if( !settings.throttle ) {
19946
            module.debug('Sending request', data, ajaxSettings.method);
19947
            module.send.request();
19948
          }
19949
          else {
19950
            if(!settings.throttleFirstRequest && !module.timer) {
19951
              module.debug('Sending request', data, ajaxSettings.method);
19952
              module.send.request();
19953
              module.timer = setTimeout(function(){}, settings.throttle);
19954
            }
19955
            else {
19956
              module.debug('Throttling request', settings.throttle);
19957
              clearTimeout(module.timer);
19958
              module.timer = setTimeout(function() {
19959
                if(module.timer) {
19960
                  delete module.timer;
19961
                }
19962
                module.debug('Sending throttled request', data, ajaxSettings.method);
19963
                module.send.request();
19964
              }, settings.throttle);
19965
            }
19966
          }
19967
19968
        },
19969
19970
        should: {
19971
          removeError: function() {
19972
            return ( settings.hideError === true || (settings.hideError === 'auto' && !module.is.form()) );
19973
          }
19974
        },
19975
19976
        is: {
19977
          disabled: function() {
19978
            return ($module.filter(selector.disabled).length > 0);
19979
          },
19980
          expectingJSON: function() {
19981
            return settings.dataType === 'json' || settings.dataType === 'jsonp';
19982
          },
19983
          form: function() {
19984
            return $module.is('form') || $context.is('form');
19985
          },
19986
          mocked: function() {
19987
            return (settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync);
19988
          },
19989
          input: function() {
19990
            return $module.is('input');
19991
          },
19992
          loading: function() {
19993
            return (module.request)
19994
              ? (module.request.state() == 'pending')
19995
              : false
19996
            ;
19997
          },
19998
          abortedRequest: function(xhr) {
19999
            if(xhr && xhr.readyState !== undefined && xhr.readyState === 0) {
20000
              module.verbose('XHR request determined to be aborted');
20001
              return true;
20002
            }
20003
            else {
20004
              module.verbose('XHR request was not aborted');
20005
              return false;
20006
            }
20007
          },
20008
          validResponse: function(response) {
20009
            if( (!module.is.expectingJSON()) || !$.isFunction(settings.successTest) ) {
20010
              module.verbose('Response is not JSON, skipping validation', settings.successTest, response);
20011
              return true;
20012
            }
20013
            module.debug('Checking JSON returned success', settings.successTest, response);
20014
            if( settings.successTest(response) ) {
20015
              module.debug('Response passed success test', response);
20016
              return true;
20017
            }
20018
            else {
20019
              module.debug('Response failed success test', response);
20020
              return false;
20021
            }
20022
          }
20023
        },
20024
20025
        was: {
20026
          cancelled: function() {
20027
            return (module.cancelled || false);
20028
          },
20029
          succesful: function() {
20030
            return (module.request && module.request.state() == 'resolved');
20031
          },
20032
          failure: function() {
20033
            return (module.request && module.request.state() == 'rejected');
20034
          },
20035
          complete: function() {
20036
            return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
20037
          }
20038
        },
20039
20040
        add: {
20041
          urlData: function(url, urlData) {
20042
            var
20043
              requiredVariables,
20044
              optionalVariables
20045
            ;
20046
            if(url) {
20047
              requiredVariables = url.match(settings.regExp.required);
20048
              optionalVariables = url.match(settings.regExp.optional);
20049
              urlData           = urlData || settings.urlData;
20050
              if(requiredVariables) {
20051
                module.debug('Looking for required URL variables', requiredVariables);
20052
                $.each(requiredVariables, function(index, templatedString) {
20053
                  var
20054
                    // allow legacy {$var} style
20055
                    variable = (templatedString.indexOf('$') !== -1)
20056
                      ? templatedString.substr(2, templatedString.length - 3)
20057
                      : templatedString.substr(1, templatedString.length - 2),
20058
                    value   = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
20059
                      ? urlData[variable]
20060
                      : ($module.data(variable) !== undefined)
20061
                        ? $module.data(variable)
20062
                        : ($context.data(variable) !== undefined)
20063
                          ? $context.data(variable)
20064
                          : urlData[variable]
20065
                  ;
20066
                  // remove value
20067
                  if(value === undefined) {
20068
                    module.error(error.requiredParameter, variable, url);
20069
                    url = false;
20070
                    return false;
20071
                  }
20072
                  else {
20073
                    module.verbose('Found required variable', variable, value);
20074
                    value = (settings.encodeParameters)
20075
                      ? module.get.urlEncodedValue(value)
20076
                      : value
20077
                    ;
20078
                    url = url.replace(templatedString, value);
20079
                  }
20080
                });
20081
              }
20082
              if(optionalVariables) {
20083
                module.debug('Looking for optional URL variables', requiredVariables);
20084
                $.each(optionalVariables, function(index, templatedString) {
20085
                  var
20086
                    // allow legacy {/$var} style
20087
                    variable = (templatedString.indexOf('$') !== -1)
20088
                      ? templatedString.substr(3, templatedString.length - 4)
20089
                      : templatedString.substr(2, templatedString.length - 3),
20090
                    value   = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
20091
                      ? urlData[variable]
20092
                      : ($module.data(variable) !== undefined)
20093
                        ? $module.data(variable)
20094
                        : ($context.data(variable) !== undefined)
20095
                          ? $context.data(variable)
20096
                          : urlData[variable]
20097
                  ;
20098
                  // optional replacement
20099
                  if(value !== undefined) {
20100
                    module.verbose('Optional variable Found', variable, value);
20101
                    url = url.replace(templatedString, value);
20102
                  }
20103
                  else {
20104
                    module.verbose('Optional variable not found', variable);
20105
                    // remove preceding slash if set
20106
                    if(url.indexOf('/' + templatedString) !== -1) {
20107
                      url = url.replace('/' + templatedString, '');
20108
                    }
20109
                    else {
20110
                      url = url.replace(templatedString, '');
20111
                    }
20112
                  }
20113
                });
20114
              }
20115
            }
20116
            return url;
20117
          },
20118
          formData: function(data) {
20119
            var
20120
              canSerialize = ($.fn.serializeObject !== undefined),
20121
              formData     = (canSerialize)
20122
                ? $form.serializeObject()
20123
                : $form.serialize(),
20124
              hasOtherData
20125
            ;
20126
            data         = data || settings.data;
20127
            hasOtherData = $.isPlainObject(data);
20128
20129
            if(hasOtherData) {
20130
              if(canSerialize) {
20131
                module.debug('Extending existing data with form data', data, formData);
20132
                data = $.extend(true, {}, data, formData);
20133
              }
20134
              else {
20135
                module.error(error.missingSerialize);
20136
                module.debug('Cant extend data. Replacing data with form data', data, formData);
20137
                data = formData;
20138
              }
20139
            }
20140
            else {
20141
              module.debug('Adding form data', formData);
20142
              data = formData;
20143
            }
20144
            return data;
20145
          }
20146
        },
20147
20148
        send: {
20149
          request: function() {
20150
            module.set.loading();
20151
            module.request = module.create.request();
20152
            if( module.is.mocked() ) {
20153
              module.mockedXHR = module.create.mockedXHR();
20154
            }
20155
            else {
20156
              module.xhr = module.create.xhr();
20157
            }
20158
            settings.onRequest.call(context, module.request, module.xhr);
20159
          }
20160
        },
20161
20162
        event: {
20163
          trigger: function(event) {
20164
            module.query();
20165
            if(event.type == 'submit' || event.type == 'click') {
20166
              event.preventDefault();
20167
            }
20168
          },
20169
          xhr: {
20170
            always: function() {
20171
              // nothing special
20172
            },
20173
            done: function(response, textStatus, xhr) {
20174
              var
20175
                context            = this,
20176
                elapsedTime        = (new Date().getTime() - requestStartTime),
20177
                timeLeft           = (settings.loadingDuration - elapsedTime),
20178
                translatedResponse = ( $.isFunction(settings.onResponse) )
20179
                  ? module.is.expectingJSON()
20180
                    ? settings.onResponse.call(context, $.extend(true, {}, response))
20181
                    : settings.onResponse.call(context, response)
20182
                  : false
20183
              ;
20184
              timeLeft = (timeLeft > 0)
20185
                ? timeLeft
20186
                : 0
20187
              ;
20188
              if(translatedResponse) {
20189
                module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response);
20190
                response = translatedResponse;
20191
              }
20192
              if(timeLeft > 0) {
20193
                module.debug('Response completed early delaying state change by', timeLeft);
20194
              }
20195
              setTimeout(function() {
20196
                if( module.is.validResponse(response) ) {
20197
                  module.request.resolveWith(context, [response, xhr]);
20198
                }
20199
                else {
20200
                  module.request.rejectWith(context, [xhr, 'invalid']);
20201
                }
20202
              }, timeLeft);
20203
            },
20204
            fail: function(xhr, status, httpMessage) {
20205
              var
20206
                context     = this,
20207
                elapsedTime = (new Date().getTime() - requestStartTime),
20208
                timeLeft    = (settings.loadingDuration - elapsedTime)
20209
              ;
20210
              timeLeft = (timeLeft > 0)
20211
                ? timeLeft
20212
                : 0
20213
              ;
20214
              if(timeLeft > 0) {
20215
                module.debug('Response completed early delaying state change by', timeLeft);
20216
              }
20217
              setTimeout(function() {
20218
                if( module.is.abortedRequest(xhr) ) {
20219
                  module.request.rejectWith(context, [xhr, 'aborted', httpMessage]);
20220
                }
20221
                else {
20222
                  module.request.rejectWith(context, [xhr, 'error', status, httpMessage]);
20223
                }
20224
              }, timeLeft);
20225
            }
20226
          },
20227
          request: {
20228
            done: function(response, xhr) {
20229
              module.debug('Successful API Response', response);
20230
              if(settings.cache === 'local' && url) {
20231
                module.write.cachedResponse(url, response);
20232
                module.debug('Saving server response locally', module.cache);
20233
              }
20234
              settings.onSuccess.call(context, response, $module, xhr);
20235
            },
20236
            complete: function(firstParameter, secondParameter) {
20237
              var
20238
                xhr,
20239
                response
20240
              ;
20241
              // have to guess callback parameters based on request success
20242
              if( module.was.succesful() ) {
20243
                response = firstParameter;
20244
                xhr      = secondParameter;
20245
              }
20246
              else {
20247
                xhr      = firstParameter;
20248
                response = module.get.responseFromXHR(xhr);
20249
              }
20250
              module.remove.loading();
20251
              settings.onComplete.call(context, response, $module, xhr);
20252
            },
20253
            fail: function(xhr, status, httpMessage) {
20254
              var
20255
                // pull response from xhr if available
20256
                response     = module.get.responseFromXHR(xhr),
20257
                errorMessage = module.get.errorFromRequest(response, status, httpMessage)
20258
              ;
20259
              if(status == 'aborted') {
20260
                module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage);
20261
                settings.onAbort.call(context, status, $module, xhr);
20262
                return true;
20263
              }
20264
              else if(status == 'invalid') {
20265
                module.debug('JSON did not pass success test. A server-side error has most likely occurred', response);
20266
              }
20267
              else if(status == 'error') {
20268
                if(xhr !== undefined) {
20269
                  module.debug('XHR produced a server error', status, httpMessage);
20270
                  // make sure we have an error to display to console
20271
                  if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
20272
                    module.error(error.statusMessage + httpMessage, ajaxSettings.url);
20273
                  }
20274
                  settings.onError.call(context, errorMessage, $module, xhr);
20275
                }
20276
              }
20277
20278
              if(settings.errorDuration && status !== 'aborted') {
20279
                module.debug('Adding error state');
20280
                module.set.error();
20281
                if( module.should.removeError() ) {
20282
                  setTimeout(module.remove.error, settings.errorDuration);
20283
                }
20284
              }
20285
              module.debug('API Request failed', errorMessage, xhr);
20286
              settings.onFailure.call(context, response, $module, xhr);
20287
            }
20288
          }
20289
        },
20290
20291
        create: {
20292
20293
          request: function() {
20294
            // api request promise
20295
            return $.Deferred()
20296
              .always(module.event.request.complete)
20297
              .done(module.event.request.done)
20298
              .fail(module.event.request.fail)
20299
            ;
20300
          },
20301
20302
          mockedXHR: function () {
20303
            var
20304
              // xhr does not simulate these properties of xhr but must return them
20305
              textStatus     = false,
20306
              status         = false,
20307
              httpMessage    = false,
20308
              responder      = settings.mockResponse      || settings.response,
20309
              asyncResponder = settings.mockResponseAsync || settings.responseAsync,
20310
              asyncCallback,
20311
              response,
20312
              mockedXHR
20313
            ;
20314
20315
            mockedXHR = $.Deferred()
20316
              .always(module.event.xhr.complete)
20317
              .done(module.event.xhr.done)
20318
              .fail(module.event.xhr.fail)
20319
            ;
20320
20321
            if(responder) {
20322
              if( $.isFunction(responder) ) {
20323
                module.debug('Using specified synchronous callback', responder);
20324
                response = responder.call(context, requestSettings);
20325
              }
20326
              else {
20327
                module.debug('Using settings specified response', responder);
20328
                response = responder;
20329
              }
20330
              // simulating response
20331
              mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
20332
            }
20333
            else if( $.isFunction(asyncResponder) ) {
20334
              asyncCallback = function(response) {
20335
                module.debug('Async callback returned response', response);
20336
20337
                if(response) {
20338
                  mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
20339
                }
20340
                else {
20341
                  mockedXHR.rejectWith(context, [{ responseText: response }, status, httpMessage]);
20342
                }
20343
              };
20344
              module.debug('Using specified async response callback', asyncResponder);
20345
              asyncResponder.call(context, requestSettings, asyncCallback);
20346
            }
20347
            return mockedXHR;
20348
          },
20349
20350
          xhr: function() {
20351
            var
20352
              xhr
20353
            ;
20354
            // ajax request promise
20355
            xhr = $.ajax(ajaxSettings)
20356
              .always(module.event.xhr.always)
20357
              .done(module.event.xhr.done)
20358
              .fail(module.event.xhr.fail)
20359
            ;
20360
            module.verbose('Created server request', xhr, ajaxSettings);
20361
            return xhr;
20362
          }
20363
        },
20364
20365
        set: {
20366
          error: function() {
20367
            module.verbose('Adding error state to element', $context);
20368
            $context.addClass(className.error);
20369
          },
20370
          loading: function() {
20371
            module.verbose('Adding loading state to element', $context);
20372
            $context.addClass(className.loading);
20373
            requestStartTime = new Date().getTime();
20374
          }
20375
        },
20376
20377
        remove: {
20378
          error: function() {
20379
            module.verbose('Removing error state from element', $context);
20380
            $context.removeClass(className.error);
20381
          },
20382
          loading: function() {
20383
            module.verbose('Removing loading state from element', $context);
20384
            $context.removeClass(className.loading);
20385
          }
20386
        },
20387
20388
        get: {
20389
          responseFromXHR: function(xhr) {
20390
            return $.isPlainObject(xhr)
20391
              ? (module.is.expectingJSON())
20392
                ? module.decode.json(xhr.responseText)
20393
                : xhr.responseText
20394
              : false
20395
            ;
20396
          },
20397
          errorFromRequest: function(response, status, httpMessage) {
20398
            return ($.isPlainObject(response) && response.error !== undefined)
20399
              ? response.error // use json error message
20400
              : (settings.error[status] !== undefined) // use server error message
20401
                ? settings.error[status]
20402
                : httpMessage
20403
            ;
20404
          },
20405
          request: function() {
20406
            return module.request || false;
20407
          },
20408
          xhr: function() {
20409
            return module.xhr || false;
20410
          },
20411
          settings: function() {
20412
            var
20413
              runSettings
20414
            ;
20415
            runSettings = settings.beforeSend.call(context, settings);
20416
            if(runSettings) {
20417
              if(runSettings.success !== undefined) {
20418
                module.debug('Legacy success callback detected', runSettings);
20419
                module.error(error.legacyParameters, runSettings.success);
20420
                runSettings.onSuccess = runSettings.success;
20421
              }
20422
              if(runSettings.failure !== undefined) {
20423
                module.debug('Legacy failure callback detected', runSettings);
20424
                module.error(error.legacyParameters, runSettings.failure);
20425
                runSettings.onFailure = runSettings.failure;
20426
              }
20427
              if(runSettings.complete !== undefined) {
20428
                module.debug('Legacy complete callback detected', runSettings);
20429
                module.error(error.legacyParameters, runSettings.complete);
20430
                runSettings.onComplete = runSettings.complete;
20431
              }
20432
            }
20433
            if(runSettings === undefined) {
20434
              module.error(error.noReturnedValue);
20435
            }
20436
            if(runSettings === false) {
20437
              return runSettings;
20438
            }
20439
            return (runSettings !== undefined)
20440
              ? $.extend(true, {}, runSettings)
20441
              : $.extend(true, {}, settings)
20442
            ;
20443
          },
20444
          urlEncodedValue: function(value) {
20445
            var
20446
              decodedValue   = window.decodeURIComponent(value),
20447
              encodedValue   = window.encodeURIComponent(value),
20448
              alreadyEncoded = (decodedValue !== value)
20449
            ;
20450
            if(alreadyEncoded) {
20451
              module.debug('URL value is already encoded, avoiding double encoding', value);
20452
              return value;
20453
            }
20454
            module.verbose('Encoding value using encodeURIComponent', value, encodedValue);
20455
            return encodedValue;
20456
          },
20457
          defaultData: function() {
20458
            var
20459
              data = {}
20460
            ;
20461
            if( !$.isWindow(element) ) {
20462
              if( module.is.input() ) {
20463
                data.value = $module.val();
20464
              }
20465
              else if( module.is.form() ) {
20466
20467
              }
20468
              else {
20469
                data.text = $module.text();
20470
              }
20471
            }
20472
            return data;
20473
          },
20474
          event: function() {
20475
            if( $.isWindow(element) || settings.on == 'now' ) {
20476
              module.debug('API called without element, no events attached');
20477
              return false;
20478
            }
20479
            else if(settings.on == 'auto') {
20480
              if( $module.is('input') ) {
20481
                return (element.oninput !== undefined)
20482
                  ? 'input'
20483
                  : (element.onpropertychange !== undefined)
20484
                    ? 'propertychange'
20485
                    : 'keyup'
20486
                ;
20487
              }
20488
              else if( $module.is('form') ) {
20489
                return 'submit';
20490
              }
20491
              else {
20492
                return 'click';
20493
              }
20494
            }
20495
            else {
20496
              return settings.on;
20497
            }
20498
          },
20499
          templatedURL: function(action) {
20500
            action = action || $module.data(metadata.action) || settings.action || false;
20501
            url    = $module.data(metadata.url) || settings.url || false;
20502
            if(url) {
20503
              module.debug('Using specified url', url);
20504
              return url;
20505
            }
20506
            if(action) {
20507
              module.debug('Looking up url for action', action, settings.api);
20508
              if(settings.api[action] === undefined && !module.is.mocked()) {
20509
                module.error(error.missingAction, settings.action, settings.api);
20510
                return;
20511
              }
20512
              url = settings.api[action];
20513
            }
20514
            else if( module.is.form() ) {
20515
              url = $module.attr('action') || $context.attr('action') || false;
20516
              module.debug('No url or action specified, defaulting to form action', url);
20517
            }
20518
            return url;
20519
          }
20520
        },
20521
20522
        abort: function() {
20523
          var
20524
            xhr = module.get.xhr()
20525
          ;
20526
          if( xhr && xhr.state() !== 'resolved') {
20527
            module.debug('Cancelling API request');
20528
            xhr.abort();
20529
          }
20530
        },
20531
20532
        // reset state
20533
        reset: function() {
20534
          module.remove.error();
20535
          module.remove.loading();
20536
        },
20537
20538
        setting: function(name, value) {
20539
          module.debug('Changing setting', name, value);
20540
          if( $.isPlainObject(name) ) {
20541
            $.extend(true, settings, name);
20542
          }
20543
          else if(value !== undefined) {
20544
            if($.isPlainObject(settings[name])) {
20545
              $.extend(true, settings[name], value);
20546
            }
20547
            else {
20548
              settings[name] = value;
20549
            }
20550
          }
20551
          else {
20552
            return settings[name];
20553
          }
20554
        },
20555
        internal: function(name, value) {
20556
          if( $.isPlainObject(name) ) {
20557
            $.extend(true, module, name);
20558
          }
20559
          else if(value !== undefined) {
20560
            module[name] = value;
20561
          }
20562
          else {
20563
            return module[name];
20564
          }
20565
        },
20566
        debug: function() {
20567
          if(!settings.silent && settings.debug) {
20568
            if(settings.performance) {
20569
              module.performance.log(arguments);
20570
            }
20571
            else {
20572
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
20573
              module.debug.apply(console, arguments);
20574
            }
20575
          }
20576
        },
20577
        verbose: function() {
20578
          if(!settings.silent && settings.verbose && settings.debug) {
20579
            if(settings.performance) {
20580
              module.performance.log(arguments);
20581
            }
20582
            else {
20583
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
20584
              module.verbose.apply(console, arguments);
20585
            }
20586
          }
20587
        },
20588
        error: function() {
20589
          if(!settings.silent) {
20590
            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
20591
            module.error.apply(console, arguments);
20592
          }
20593
        },
20594
        performance: {
20595
          log: function(message) {
20596
            var
20597
              currentTime,
20598
              executionTime,
20599
              previousTime
20600
            ;
20601
            if(settings.performance) {
20602
              currentTime   = new Date().getTime();
20603
              previousTime  = time || currentTime;
20604
              executionTime = currentTime - previousTime;
20605
              time          = currentTime;
20606
              performance.push({
20607
                'Name'           : message[0],
20608
                'Arguments'      : [].slice.call(message, 1) || '',
20609
                //'Element'        : element,
20610
                'Execution Time' : executionTime
20611
              });
20612
            }
20613
            clearTimeout(module.performance.timer);
20614
            module.performance.timer = setTimeout(module.performance.display, 500);
20615
          },
20616
          display: function() {
20617
            var
20618
              title = settings.name + ':',
20619
              totalTime = 0
20620
            ;
20621
            time = false;
20622
            clearTimeout(module.performance.timer);
20623
            $.each(performance, function(index, data) {
20624
              totalTime += data['Execution Time'];
20625
            });
20626
            title += ' ' + totalTime + 'ms';
20627
            if(moduleSelector) {
20628
              title += ' \'' + moduleSelector + '\'';
20629
            }
20630
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
20631
              console.groupCollapsed(title);
20632
              if(console.table) {
20633
                console.table(performance);
20634
              }
20635
              else {
20636
                $.each(performance, function(index, data) {
20637
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
20638
                });
20639
              }
20640
              console.groupEnd();
20641
            }
20642
            performance = [];
20643
          }
20644
        },
20645
        invoke: function(query, passedArguments, context) {
20646
          var
20647
            object = instance,
20648
            maxDepth,
20649
            found,
20650
            response
20651
          ;
20652
          passedArguments = passedArguments || queryArguments;
20653
          context         = element         || context;
20654
          if(typeof query == 'string' && object !== undefined) {
20655
            query    = query.split(/[\. ]/);
20656
            maxDepth = query.length - 1;
20657
            $.each(query, function(depth, value) {
20658
              var camelCaseValue = (depth != maxDepth)
20659
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
20660
                : query
20661
              ;
20662
              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
20663
                object = object[camelCaseValue];
20664
              }
20665
              else if( object[camelCaseValue] !== undefined ) {
20666
                found = object[camelCaseValue];
20667
                return false;
20668
              }
20669
              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
20670
                object = object[value];
20671
              }
20672
              else if( object[value] !== undefined ) {
20673
                found = object[value];
20674
                return false;
20675
              }
20676
              else {
20677
                module.error(error.method, query);
20678
                return false;
20679
              }
20680
            });
20681
          }
20682
          if ( $.isFunction( found ) ) {
20683
            response = found.apply(context, passedArguments);
20684
          }
20685
          else if(found !== undefined) {
20686
            response = found;
20687
          }
20688
          if($.isArray(returnedValue)) {
20689
            returnedValue.push(response);
20690
          }
20691
          else if(returnedValue !== undefined) {
20692
            returnedValue = [returnedValue, response];
20693
          }
20694
          else if(response !== undefined) {
20695
            returnedValue = response;
20696
          }
20697
          return found;
20698
        }
20699
      };
20700
20701
      if(methodInvoked) {
20702
        if(instance === undefined) {
20703
          module.initialize();
20704
        }
20705
        module.invoke(query);
20706
      }
20707
      else {
20708
        if(instance !== undefined) {
20709
          instance.invoke('destroy');
20710
        }
20711
        module.initialize();
20712
      }
20713
    })
20714
  ;
20715
20716
  return (returnedValue !== undefined)
20717
    ? returnedValue
20718
    : this
20719
  ;
20720
};
20721
20722
$.api.settings = {
20723
20724
  name              : 'API',
20725
  namespace         : 'api',
20726
20727
  debug             : false,
20728
  verbose           : false,
20729
  performance       : true,
20730
20731
  // object containing all templates endpoints
20732
  api               : {},
20733
20734
  // whether to cache responses
20735
  cache             : true,
20736
20737
  // whether new requests should abort previous requests
20738
  interruptRequests : true,
20739
20740
  // event binding
20741
  on                : 'auto',
20742
20743
  // context for applying state classes
20744
  stateContext      : false,
20745
20746
  // duration for loading state
20747
  loadingDuration   : 0,
20748
20749
  // whether to hide errors after a period of time
20750
  hideError         : 'auto',
20751
20752
  // duration for error state
20753
  errorDuration     : 2000,
20754
20755
  // whether parameters should be encoded with encodeURIComponent
20756
  encodeParameters  : true,
20757
20758
  // API action to use
20759
  action            : false,
20760
20761
  // templated URL to use
20762
  url               : false,
20763
20764
  // base URL to apply to all endpoints
20765
  base              : '',
20766
20767
  // data that will
20768
  urlData           : {},
20769
20770
  // whether to add default data to url data
20771
  defaultData          : true,
20772
20773
  // whether to serialize closest form
20774
  serializeForm        : false,
20775
20776
  // how long to wait before request should occur
20777
  throttle             : 0,
20778
20779
  // whether to throttle first request or only repeated
20780
  throttleFirstRequest : true,
20781
20782
  // standard ajax settings
20783
  method            : 'get',
20784
  data              : {},
20785
  dataType          : 'json',
20786
20787
  // mock response
20788
  mockResponse      : false,
20789
  mockResponseAsync : false,
20790
20791
  // aliases for mock
20792
  response          : false,
20793
  responseAsync     : false,
20794
20795
  // callbacks before request
20796
  beforeSend  : function(settings) { return settings; },
20797
  beforeXHR   : function(xhr) {},
20798
  onRequest   : function(promise, xhr) {},
20799
20800
  // after request
20801
  onResponse  : false, // function(response) { },
20802
20803
  // response was successful, if JSON passed validation
20804
  onSuccess   : function(response, $module) {},
20805
20806
  // request finished without aborting
20807
  onComplete  : function(response, $module) {},
20808
20809
  // failed JSON success test
20810
  onFailure   : function(response, $module) {},
20811
20812
  // server error
20813
  onError     : function(errorMessage, $module) {},
20814
20815
  // request aborted
20816
  onAbort     : function(errorMessage, $module) {},
20817
20818
  successTest : false,
20819
20820
  // errors
20821
  error : {
20822
    beforeSend        : 'The before send function has aborted the request',
20823
    error             : 'There was an error with your request',
20824
    exitConditions    : 'API Request Aborted. Exit conditions met',
20825
    JSONParse         : 'JSON could not be parsed during error handling',
20826
    legacyParameters  : 'You are using legacy API success callback names',
20827
    method            : 'The method you called is not defined',
20828
    missingAction     : 'API action used but no url was defined',
20829
    missingSerialize  : 'jquery-serialize-object is required to add form data to an existing data object',
20830
    missingURL        : 'No URL specified for api event',
20831
    noReturnedValue   : 'The beforeSend callback must return a settings object, beforeSend ignored.',
20832
    noStorage         : 'Caching responses locally requires session storage',
20833
    parseError        : 'There was an error parsing your request',
20834
    requiredParameter : 'Missing a required URL parameter: ',
20835
    statusMessage     : 'Server gave an error: ',
20836
    timeout           : 'Your request timed out'
20837
  },
20838
20839
  regExp  : {
20840
    required : /\{\$*[A-z0-9]+\}/g,
20841
    optional : /\{\/\$*[A-z0-9]+\}/g,
20842
  },
20843
20844
  className: {
20845
    loading : 'loading',
20846
    error   : 'error'
20847
  },
20848
20849
  selector: {
20850
    disabled : '.disabled',
20851
    form      : 'form'
20852
  },
20853
20854
  metadata: {
20855
    action  : 'action',
20856
    url     : 'url'
20857
  }
20858
};
20859
20860
20861
20862
})( jQuery, window, document );
20863
20864
/*!
20865
 * # Semantic UI 2.2.11 - State

public/lib/semantic/components/api.js 1 location

@@ 11-1167 (lines=1157) @@
8
 *
9
 */
10
11
;(function ($, window, document, undefined) {
12
13
"use strict";
14
15
var
16
  window = (typeof window != 'undefined' && window.Math == Math)
17
    ? window
18
    : (typeof self != 'undefined' && self.Math == Math)
19
      ? self
20
      : Function('return this')()
21
;
22
23
$.api = $.fn.api = function(parameters) {
24
25
  var
26
    // use window context if none specified
27
    $allModules     = $.isFunction(this)
28
        ? $(window)
29
        : $(this),
30
    moduleSelector = $allModules.selector || '',
31
    time           = new Date().getTime(),
32
    performance    = [],
33
34
    query          = arguments[0],
35
    methodInvoked  = (typeof query == 'string'),
36
    queryArguments = [].slice.call(arguments, 1),
37
38
    returnedValue
39
  ;
40
41
  $allModules
42
    .each(function() {
43
      var
44
        settings          = ( $.isPlainObject(parameters) )
45
          ? $.extend(true, {}, $.fn.api.settings, parameters)
46
          : $.extend({}, $.fn.api.settings),
47
48
        // internal aliases
49
        namespace       = settings.namespace,
50
        metadata        = settings.metadata,
51
        selector        = settings.selector,
52
        error           = settings.error,
53
        className       = settings.className,
54
55
        // define namespaces for modules
56
        eventNamespace  = '.' + namespace,
57
        moduleNamespace = 'module-' + namespace,
58
59
        // element that creates request
60
        $module         = $(this),
61
        $form           = $module.closest(selector.form),
62
63
        // context used for state
64
        $context        = (settings.stateContext)
65
          ? $(settings.stateContext)
66
          : $module,
67
68
        // request details
69
        ajaxSettings,
70
        requestSettings,
71
        url,
72
        data,
73
        requestStartTime,
74
75
        // standard module
76
        element         = this,
77
        context         = $context[0],
78
        instance        = $module.data(moduleNamespace),
79
        module
80
      ;
81
82
      module = {
83
84
        initialize: function() {
85
          if(!methodInvoked) {
86
            module.bind.events();
87
          }
88
          module.instantiate();
89
        },
90
91
        instantiate: function() {
92
          module.verbose('Storing instance of module', module);
93
          instance = module;
94
          $module
95
            .data(moduleNamespace, instance)
96
          ;
97
        },
98
99
        destroy: function() {
100
          module.verbose('Destroying previous module for', element);
101
          $module
102
            .removeData(moduleNamespace)
103
            .off(eventNamespace)
104
          ;
105
        },
106
107
        bind: {
108
          events: function() {
109
            var
110
              triggerEvent = module.get.event()
111
            ;
112
            if( triggerEvent ) {
113
              module.verbose('Attaching API events to element', triggerEvent);
114
              $module
115
                .on(triggerEvent + eventNamespace, module.event.trigger)
116
              ;
117
            }
118
            else if(settings.on == 'now') {
119
              module.debug('Querying API endpoint immediately');
120
              module.query();
121
            }
122
          }
123
        },
124
125
        decode: {
126
          json: function(response) {
127
            if(response !== undefined && typeof response == 'string') {
128
              try {
129
               response = JSON.parse(response);
130
              }
131
              catch(e) {
132
                // isnt json string
133
              }
134
            }
135
            return response;
136
          }
137
        },
138
139
        read: {
140
          cachedResponse: function(url) {
141
            var
142
              response
143
            ;
144
            if(window.Storage === undefined) {
145
              module.error(error.noStorage);
146
              return;
147
            }
148
            response = sessionStorage.getItem(url);
149
            module.debug('Using cached response', url, response);
150
            response = module.decode.json(response);
151
            return response;
152
          }
153
        },
154
        write: {
155
          cachedResponse: function(url, response) {
156
            if(response && response === '') {
157
              module.debug('Response empty, not caching', response);
158
              return;
159
            }
160
            if(window.Storage === undefined) {
161
              module.error(error.noStorage);
162
              return;
163
            }
164
            if( $.isPlainObject(response) ) {
165
              response = JSON.stringify(response);
166
            }
167
            sessionStorage.setItem(url, response);
168
            module.verbose('Storing cached response for url', url, response);
169
          }
170
        },
171
172
        query: function() {
173
174
          if(module.is.disabled()) {
175
            module.debug('Element is disabled API request aborted');
176
            return;
177
          }
178
179
          if(module.is.loading()) {
180
            if(settings.interruptRequests) {
181
              module.debug('Interrupting previous request');
182
              module.abort();
183
            }
184
            else {
185
              module.debug('Cancelling request, previous request is still pending');
186
              return;
187
            }
188
          }
189
190
          // pass element metadata to url (value, text)
191
          if(settings.defaultData) {
192
            $.extend(true, settings.urlData, module.get.defaultData());
193
          }
194
195
          // Add form content
196
          if(settings.serializeForm) {
197
            settings.data = module.add.formData(settings.data);
198
          }
199
200
          // call beforesend and get any settings changes
201
          requestSettings = module.get.settings();
202
203
          // check if before send cancelled request
204
          if(requestSettings === false) {
205
            module.cancelled = true;
206
            module.error(error.beforeSend);
207
            return;
208
          }
209
          else {
210
            module.cancelled = false;
211
          }
212
213
          // get url
214
          url = module.get.templatedURL();
215
216
          if(!url && !module.is.mocked()) {
217
            module.error(error.missingURL);
218
            return;
219
          }
220
221
          // replace variables
222
          url = module.add.urlData( url );
223
          // missing url parameters
224
          if( !url && !module.is.mocked()) {
225
            return;
226
          }
227
228
          requestSettings.url = settings.base + url;
229
230
          // look for jQuery ajax parameters in settings
231
          ajaxSettings = $.extend(true, {}, settings, {
232
            type       : settings.method || settings.type,
233
            data       : data,
234
            url        : settings.base + url,
235
            beforeSend : settings.beforeXHR,
236
            success    : function() {},
237
            failure    : function() {},
238
            complete   : function() {}
239
          });
240
241
          module.debug('Querying URL', ajaxSettings.url);
242
          module.verbose('Using AJAX settings', ajaxSettings);
243
          if(settings.cache === 'local' && module.read.cachedResponse(url)) {
244
            module.debug('Response returned from local cache');
245
            module.request = module.create.request();
246
            module.request.resolveWith(context, [ module.read.cachedResponse(url) ]);
247
            return;
248
          }
249
250
          if( !settings.throttle ) {
251
            module.debug('Sending request', data, ajaxSettings.method);
252
            module.send.request();
253
          }
254
          else {
255
            if(!settings.throttleFirstRequest && !module.timer) {
256
              module.debug('Sending request', data, ajaxSettings.method);
257
              module.send.request();
258
              module.timer = setTimeout(function(){}, settings.throttle);
259
            }
260
            else {
261
              module.debug('Throttling request', settings.throttle);
262
              clearTimeout(module.timer);
263
              module.timer = setTimeout(function() {
264
                if(module.timer) {
265
                  delete module.timer;
266
                }
267
                module.debug('Sending throttled request', data, ajaxSettings.method);
268
                module.send.request();
269
              }, settings.throttle);
270
            }
271
          }
272
273
        },
274
275
        should: {
276
          removeError: function() {
277
            return ( settings.hideError === true || (settings.hideError === 'auto' && !module.is.form()) );
278
          }
279
        },
280
281
        is: {
282
          disabled: function() {
283
            return ($module.filter(selector.disabled).length > 0);
284
          },
285
          expectingJSON: function() {
286
            return settings.dataType === 'json' || settings.dataType === 'jsonp';
287
          },
288
          form: function() {
289
            return $module.is('form') || $context.is('form');
290
          },
291
          mocked: function() {
292
            return (settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync);
293
          },
294
          input: function() {
295
            return $module.is('input');
296
          },
297
          loading: function() {
298
            return (module.request)
299
              ? (module.request.state() == 'pending')
300
              : false
301
            ;
302
          },
303
          abortedRequest: function(xhr) {
304
            if(xhr && xhr.readyState !== undefined && xhr.readyState === 0) {
305
              module.verbose('XHR request determined to be aborted');
306
              return true;
307
            }
308
            else {
309
              module.verbose('XHR request was not aborted');
310
              return false;
311
            }
312
          },
313
          validResponse: function(response) {
314
            if( (!module.is.expectingJSON()) || !$.isFunction(settings.successTest) ) {
315
              module.verbose('Response is not JSON, skipping validation', settings.successTest, response);
316
              return true;
317
            }
318
            module.debug('Checking JSON returned success', settings.successTest, response);
319
            if( settings.successTest(response) ) {
320
              module.debug('Response passed success test', response);
321
              return true;
322
            }
323
            else {
324
              module.debug('Response failed success test', response);
325
              return false;
326
            }
327
          }
328
        },
329
330
        was: {
331
          cancelled: function() {
332
            return (module.cancelled || false);
333
          },
334
          succesful: function() {
335
            return (module.request && module.request.state() == 'resolved');
336
          },
337
          failure: function() {
338
            return (module.request && module.request.state() == 'rejected');
339
          },
340
          complete: function() {
341
            return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
342
          }
343
        },
344
345
        add: {
346
          urlData: function(url, urlData) {
347
            var
348
              requiredVariables,
349
              optionalVariables
350
            ;
351
            if(url) {
352
              requiredVariables = url.match(settings.regExp.required);
353
              optionalVariables = url.match(settings.regExp.optional);
354
              urlData           = urlData || settings.urlData;
355
              if(requiredVariables) {
356
                module.debug('Looking for required URL variables', requiredVariables);
357
                $.each(requiredVariables, function(index, templatedString) {
358
                  var
359
                    // allow legacy {$var} style
360
                    variable = (templatedString.indexOf('$') !== -1)
361
                      ? templatedString.substr(2, templatedString.length - 3)
362
                      : templatedString.substr(1, templatedString.length - 2),
363
                    value   = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
364
                      ? urlData[variable]
365
                      : ($module.data(variable) !== undefined)
366
                        ? $module.data(variable)
367
                        : ($context.data(variable) !== undefined)
368
                          ? $context.data(variable)
369
                          : urlData[variable]
370
                  ;
371
                  // remove value
372
                  if(value === undefined) {
373
                    module.error(error.requiredParameter, variable, url);
374
                    url = false;
375
                    return false;
376
                  }
377
                  else {
378
                    module.verbose('Found required variable', variable, value);
379
                    value = (settings.encodeParameters)
380
                      ? module.get.urlEncodedValue(value)
381
                      : value
382
                    ;
383
                    url = url.replace(templatedString, value);
384
                  }
385
                });
386
              }
387
              if(optionalVariables) {
388
                module.debug('Looking for optional URL variables', requiredVariables);
389
                $.each(optionalVariables, function(index, templatedString) {
390
                  var
391
                    // allow legacy {/$var} style
392
                    variable = (templatedString.indexOf('$') !== -1)
393
                      ? templatedString.substr(3, templatedString.length - 4)
394
                      : templatedString.substr(2, templatedString.length - 3),
395
                    value   = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
396
                      ? urlData[variable]
397
                      : ($module.data(variable) !== undefined)
398
                        ? $module.data(variable)
399
                        : ($context.data(variable) !== undefined)
400
                          ? $context.data(variable)
401
                          : urlData[variable]
402
                  ;
403
                  // optional replacement
404
                  if(value !== undefined) {
405
                    module.verbose('Optional variable Found', variable, value);
406
                    url = url.replace(templatedString, value);
407
                  }
408
                  else {
409
                    module.verbose('Optional variable not found', variable);
410
                    // remove preceding slash if set
411
                    if(url.indexOf('/' + templatedString) !== -1) {
412
                      url = url.replace('/' + templatedString, '');
413
                    }
414
                    else {
415
                      url = url.replace(templatedString, '');
416
                    }
417
                  }
418
                });
419
              }
420
            }
421
            return url;
422
          },
423
          formData: function(data) {
424
            var
425
              canSerialize = ($.fn.serializeObject !== undefined),
426
              formData     = (canSerialize)
427
                ? $form.serializeObject()
428
                : $form.serialize(),
429
              hasOtherData
430
            ;
431
            data         = data || settings.data;
432
            hasOtherData = $.isPlainObject(data);
433
434
            if(hasOtherData) {
435
              if(canSerialize) {
436
                module.debug('Extending existing data with form data', data, formData);
437
                data = $.extend(true, {}, data, formData);
438
              }
439
              else {
440
                module.error(error.missingSerialize);
441
                module.debug('Cant extend data. Replacing data with form data', data, formData);
442
                data = formData;
443
              }
444
            }
445
            else {
446
              module.debug('Adding form data', formData);
447
              data = formData;
448
            }
449
            return data;
450
          }
451
        },
452
453
        send: {
454
          request: function() {
455
            module.set.loading();
456
            module.request = module.create.request();
457
            if( module.is.mocked() ) {
458
              module.mockedXHR = module.create.mockedXHR();
459
            }
460
            else {
461
              module.xhr = module.create.xhr();
462
            }
463
            settings.onRequest.call(context, module.request, module.xhr);
464
          }
465
        },
466
467
        event: {
468
          trigger: function(event) {
469
            module.query();
470
            if(event.type == 'submit' || event.type == 'click') {
471
              event.preventDefault();
472
            }
473
          },
474
          xhr: {
475
            always: function() {
476
              // nothing special
477
            },
478
            done: function(response, textStatus, xhr) {
479
              var
480
                context            = this,
481
                elapsedTime        = (new Date().getTime() - requestStartTime),
482
                timeLeft           = (settings.loadingDuration - elapsedTime),
483
                translatedResponse = ( $.isFunction(settings.onResponse) )
484
                  ? module.is.expectingJSON()
485
                    ? settings.onResponse.call(context, $.extend(true, {}, response))
486
                    : settings.onResponse.call(context, response)
487
                  : false
488
              ;
489
              timeLeft = (timeLeft > 0)
490
                ? timeLeft
491
                : 0
492
              ;
493
              if(translatedResponse) {
494
                module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response);
495
                response = translatedResponse;
496
              }
497
              if(timeLeft > 0) {
498
                module.debug('Response completed early delaying state change by', timeLeft);
499
              }
500
              setTimeout(function() {
501
                if( module.is.validResponse(response) ) {
502
                  module.request.resolveWith(context, [response, xhr]);
503
                }
504
                else {
505
                  module.request.rejectWith(context, [xhr, 'invalid']);
506
                }
507
              }, timeLeft);
508
            },
509
            fail: function(xhr, status, httpMessage) {
510
              var
511
                context     = this,
512
                elapsedTime = (new Date().getTime() - requestStartTime),
513
                timeLeft    = (settings.loadingDuration - elapsedTime)
514
              ;
515
              timeLeft = (timeLeft > 0)
516
                ? timeLeft
517
                : 0
518
              ;
519
              if(timeLeft > 0) {
520
                module.debug('Response completed early delaying state change by', timeLeft);
521
              }
522
              setTimeout(function() {
523
                if( module.is.abortedRequest(xhr) ) {
524
                  module.request.rejectWith(context, [xhr, 'aborted', httpMessage]);
525
                }
526
                else {
527
                  module.request.rejectWith(context, [xhr, 'error', status, httpMessage]);
528
                }
529
              }, timeLeft);
530
            }
531
          },
532
          request: {
533
            done: function(response, xhr) {
534
              module.debug('Successful API Response', response);
535
              if(settings.cache === 'local' && url) {
536
                module.write.cachedResponse(url, response);
537
                module.debug('Saving server response locally', module.cache);
538
              }
539
              settings.onSuccess.call(context, response, $module, xhr);
540
            },
541
            complete: function(firstParameter, secondParameter) {
542
              var
543
                xhr,
544
                response
545
              ;
546
              // have to guess callback parameters based on request success
547
              if( module.was.succesful() ) {
548
                response = firstParameter;
549
                xhr      = secondParameter;
550
              }
551
              else {
552
                xhr      = firstParameter;
553
                response = module.get.responseFromXHR(xhr);
554
              }
555
              module.remove.loading();
556
              settings.onComplete.call(context, response, $module, xhr);
557
            },
558
            fail: function(xhr, status, httpMessage) {
559
              var
560
                // pull response from xhr if available
561
                response     = module.get.responseFromXHR(xhr),
562
                errorMessage = module.get.errorFromRequest(response, status, httpMessage)
563
              ;
564
              if(status == 'aborted') {
565
                module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage);
566
                settings.onAbort.call(context, status, $module, xhr);
567
                return true;
568
              }
569
              else if(status == 'invalid') {
570
                module.debug('JSON did not pass success test. A server-side error has most likely occurred', response);
571
              }
572
              else if(status == 'error') {
573
                if(xhr !== undefined) {
574
                  module.debug('XHR produced a server error', status, httpMessage);
575
                  // make sure we have an error to display to console
576
                  if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
577
                    module.error(error.statusMessage + httpMessage, ajaxSettings.url);
578
                  }
579
                  settings.onError.call(context, errorMessage, $module, xhr);
580
                }
581
              }
582
583
              if(settings.errorDuration && status !== 'aborted') {
584
                module.debug('Adding error state');
585
                module.set.error();
586
                if( module.should.removeError() ) {
587
                  setTimeout(module.remove.error, settings.errorDuration);
588
                }
589
              }
590
              module.debug('API Request failed', errorMessage, xhr);
591
              settings.onFailure.call(context, response, $module, xhr);
592
            }
593
          }
594
        },
595
596
        create: {
597
598
          request: function() {
599
            // api request promise
600
            return $.Deferred()
601
              .always(module.event.request.complete)
602
              .done(module.event.request.done)
603
              .fail(module.event.request.fail)
604
            ;
605
          },
606
607
          mockedXHR: function () {
608
            var
609
              // xhr does not simulate these properties of xhr but must return them
610
              textStatus     = false,
611
              status         = false,
612
              httpMessage    = false,
613
              responder      = settings.mockResponse      || settings.response,
614
              asyncResponder = settings.mockResponseAsync || settings.responseAsync,
615
              asyncCallback,
616
              response,
617
              mockedXHR
618
            ;
619
620
            mockedXHR = $.Deferred()
621
              .always(module.event.xhr.complete)
622
              .done(module.event.xhr.done)
623
              .fail(module.event.xhr.fail)
624
            ;
625
626
            if(responder) {
627
              if( $.isFunction(responder) ) {
628
                module.debug('Using specified synchronous callback', responder);
629
                response = responder.call(context, requestSettings);
630
              }
631
              else {
632
                module.debug('Using settings specified response', responder);
633
                response = responder;
634
              }
635
              // simulating response
636
              mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
637
            }
638
            else if( $.isFunction(asyncResponder) ) {
639
              asyncCallback = function(response) {
640
                module.debug('Async callback returned response', response);
641
642
                if(response) {
643
                  mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
644
                }
645
                else {
646
                  mockedXHR.rejectWith(context, [{ responseText: response }, status, httpMessage]);
647
                }
648
              };
649
              module.debug('Using specified async response callback', asyncResponder);
650
              asyncResponder.call(context, requestSettings, asyncCallback);
651
            }
652
            return mockedXHR;
653
          },
654
655
          xhr: function() {
656
            var
657
              xhr
658
            ;
659
            // ajax request promise
660
            xhr = $.ajax(ajaxSettings)
661
              .always(module.event.xhr.always)
662
              .done(module.event.xhr.done)
663
              .fail(module.event.xhr.fail)
664
            ;
665
            module.verbose('Created server request', xhr, ajaxSettings);
666
            return xhr;
667
          }
668
        },
669
670
        set: {
671
          error: function() {
672
            module.verbose('Adding error state to element', $context);
673
            $context.addClass(className.error);
674
          },
675
          loading: function() {
676
            module.verbose('Adding loading state to element', $context);
677
            $context.addClass(className.loading);
678
            requestStartTime = new Date().getTime();
679
          }
680
        },
681
682
        remove: {
683
          error: function() {
684
            module.verbose('Removing error state from element', $context);
685
            $context.removeClass(className.error);
686
          },
687
          loading: function() {
688
            module.verbose('Removing loading state from element', $context);
689
            $context.removeClass(className.loading);
690
          }
691
        },
692
693
        get: {
694
          responseFromXHR: function(xhr) {
695
            return $.isPlainObject(xhr)
696
              ? (module.is.expectingJSON())
697
                ? module.decode.json(xhr.responseText)
698
                : xhr.responseText
699
              : false
700
            ;
701
          },
702
          errorFromRequest: function(response, status, httpMessage) {
703
            return ($.isPlainObject(response) && response.error !== undefined)
704
              ? response.error // use json error message
705
              : (settings.error[status] !== undefined) // use server error message
706
                ? settings.error[status]
707
                : httpMessage
708
            ;
709
          },
710
          request: function() {
711
            return module.request || false;
712
          },
713
          xhr: function() {
714
            return module.xhr || false;
715
          },
716
          settings: function() {
717
            var
718
              runSettings
719
            ;
720
            runSettings = settings.beforeSend.call(context, settings);
721
            if(runSettings) {
722
              if(runSettings.success !== undefined) {
723
                module.debug('Legacy success callback detected', runSettings);
724
                module.error(error.legacyParameters, runSettings.success);
725
                runSettings.onSuccess = runSettings.success;
726
              }
727
              if(runSettings.failure !== undefined) {
728
                module.debug('Legacy failure callback detected', runSettings);
729
                module.error(error.legacyParameters, runSettings.failure);
730
                runSettings.onFailure = runSettings.failure;
731
              }
732
              if(runSettings.complete !== undefined) {
733
                module.debug('Legacy complete callback detected', runSettings);
734
                module.error(error.legacyParameters, runSettings.complete);
735
                runSettings.onComplete = runSettings.complete;
736
              }
737
            }
738
            if(runSettings === undefined) {
739
              module.error(error.noReturnedValue);
740
            }
741
            if(runSettings === false) {
742
              return runSettings;
743
            }
744
            return (runSettings !== undefined)
745
              ? $.extend(true, {}, runSettings)
746
              : $.extend(true, {}, settings)
747
            ;
748
          },
749
          urlEncodedValue: function(value) {
750
            var
751
              decodedValue   = window.decodeURIComponent(value),
752
              encodedValue   = window.encodeURIComponent(value),
753
              alreadyEncoded = (decodedValue !== value)
754
            ;
755
            if(alreadyEncoded) {
756
              module.debug('URL value is already encoded, avoiding double encoding', value);
757
              return value;
758
            }
759
            module.verbose('Encoding value using encodeURIComponent', value, encodedValue);
760
            return encodedValue;
761
          },
762
          defaultData: function() {
763
            var
764
              data = {}
765
            ;
766
            if( !$.isWindow(element) ) {
767
              if( module.is.input() ) {
768
                data.value = $module.val();
769
              }
770
              else if( module.is.form() ) {
771
772
              }
773
              else {
774
                data.text = $module.text();
775
              }
776
            }
777
            return data;
778
          },
779
          event: function() {
780
            if( $.isWindow(element) || settings.on == 'now' ) {
781
              module.debug('API called without element, no events attached');
782
              return false;
783
            }
784
            else if(settings.on == 'auto') {
785
              if( $module.is('input') ) {
786
                return (element.oninput !== undefined)
787
                  ? 'input'
788
                  : (element.onpropertychange !== undefined)
789
                    ? 'propertychange'
790
                    : 'keyup'
791
                ;
792
              }
793
              else if( $module.is('form') ) {
794
                return 'submit';
795
              }
796
              else {
797
                return 'click';
798
              }
799
            }
800
            else {
801
              return settings.on;
802
            }
803
          },
804
          templatedURL: function(action) {
805
            action = action || $module.data(metadata.action) || settings.action || false;
806
            url    = $module.data(metadata.url) || settings.url || false;
807
            if(url) {
808
              module.debug('Using specified url', url);
809
              return url;
810
            }
811
            if(action) {
812
              module.debug('Looking up url for action', action, settings.api);
813
              if(settings.api[action] === undefined && !module.is.mocked()) {
814
                module.error(error.missingAction, settings.action, settings.api);
815
                return;
816
              }
817
              url = settings.api[action];
818
            }
819
            else if( module.is.form() ) {
820
              url = $module.attr('action') || $context.attr('action') || false;
821
              module.debug('No url or action specified, defaulting to form action', url);
822
            }
823
            return url;
824
          }
825
        },
826
827
        abort: function() {
828
          var
829
            xhr = module.get.xhr()
830
          ;
831
          if( xhr && xhr.state() !== 'resolved') {
832
            module.debug('Cancelling API request');
833
            xhr.abort();
834
          }
835
        },
836
837
        // reset state
838
        reset: function() {
839
          module.remove.error();
840
          module.remove.loading();
841
        },
842
843
        setting: function(name, value) {
844
          module.debug('Changing setting', name, value);
845
          if( $.isPlainObject(name) ) {
846
            $.extend(true, settings, name);
847
          }
848
          else if(value !== undefined) {
849
            if($.isPlainObject(settings[name])) {
850
              $.extend(true, settings[name], value);
851
            }
852
            else {
853
              settings[name] = value;
854
            }
855
          }
856
          else {
857
            return settings[name];
858
          }
859
        },
860
        internal: function(name, value) {
861
          if( $.isPlainObject(name) ) {
862
            $.extend(true, module, name);
863
          }
864
          else if(value !== undefined) {
865
            module[name] = value;
866
          }
867
          else {
868
            return module[name];
869
          }
870
        },
871
        debug: function() {
872
          if(!settings.silent && settings.debug) {
873
            if(settings.performance) {
874
              module.performance.log(arguments);
875
            }
876
            else {
877
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
878
              module.debug.apply(console, arguments);
879
            }
880
          }
881
        },
882
        verbose: function() {
883
          if(!settings.silent && settings.verbose && settings.debug) {
884
            if(settings.performance) {
885
              module.performance.log(arguments);
886
            }
887
            else {
888
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
889
              module.verbose.apply(console, arguments);
890
            }
891
          }
892
        },
893
        error: function() {
894
          if(!settings.silent) {
895
            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
896
            module.error.apply(console, arguments);
897
          }
898
        },
899
        performance: {
900
          log: function(message) {
901
            var
902
              currentTime,
903
              executionTime,
904
              previousTime
905
            ;
906
            if(settings.performance) {
907
              currentTime   = new Date().getTime();
908
              previousTime  = time || currentTime;
909
              executionTime = currentTime - previousTime;
910
              time          = currentTime;
911
              performance.push({
912
                'Name'           : message[0],
913
                'Arguments'      : [].slice.call(message, 1) || '',
914
                //'Element'        : element,
915
                'Execution Time' : executionTime
916
              });
917
            }
918
            clearTimeout(module.performance.timer);
919
            module.performance.timer = setTimeout(module.performance.display, 500);
920
          },
921
          display: function() {
922
            var
923
              title = settings.name + ':',
924
              totalTime = 0
925
            ;
926
            time = false;
927
            clearTimeout(module.performance.timer);
928
            $.each(performance, function(index, data) {
929
              totalTime += data['Execution Time'];
930
            });
931
            title += ' ' + totalTime + 'ms';
932
            if(moduleSelector) {
933
              title += ' \'' + moduleSelector + '\'';
934
            }
935
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
936
              console.groupCollapsed(title);
937
              if(console.table) {
938
                console.table(performance);
939
              }
940
              else {
941
                $.each(performance, function(index, data) {
942
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
943
                });
944
              }
945
              console.groupEnd();
946
            }
947
            performance = [];
948
          }
949
        },
950
        invoke: function(query, passedArguments, context) {
951
          var
952
            object = instance,
953
            maxDepth,
954
            found,
955
            response
956
          ;
957
          passedArguments = passedArguments || queryArguments;
958
          context         = element         || context;
959
          if(typeof query == 'string' && object !== undefined) {
960
            query    = query.split(/[\. ]/);
961
            maxDepth = query.length - 1;
962
            $.each(query, function(depth, value) {
963
              var camelCaseValue = (depth != maxDepth)
964
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
965
                : query
966
              ;
967
              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
968
                object = object[camelCaseValue];
969
              }
970
              else if( object[camelCaseValue] !== undefined ) {
971
                found = object[camelCaseValue];
972
                return false;
973
              }
974
              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
975
                object = object[value];
976
              }
977
              else if( object[value] !== undefined ) {
978
                found = object[value];
979
                return false;
980
              }
981
              else {
982
                module.error(error.method, query);
983
                return false;
984
              }
985
            });
986
          }
987
          if ( $.isFunction( found ) ) {
988
            response = found.apply(context, passedArguments);
989
          }
990
          else if(found !== undefined) {
991
            response = found;
992
          }
993
          if($.isArray(returnedValue)) {
994
            returnedValue.push(response);
995
          }
996
          else if(returnedValue !== undefined) {
997
            returnedValue = [returnedValue, response];
998
          }
999
          else if(response !== undefined) {
1000
            returnedValue = response;
1001
          }
1002
          return found;
1003
        }
1004
      };
1005
1006
      if(methodInvoked) {
1007
        if(instance === undefined) {
1008
          module.initialize();
1009
        }
1010
        module.invoke(query);
1011
      }
1012
      else {
1013
        if(instance !== undefined) {
1014
          instance.invoke('destroy');
1015
        }
1016
        module.initialize();
1017
      }
1018
    })
1019
  ;
1020
1021
  return (returnedValue !== undefined)
1022
    ? returnedValue
1023
    : this
1024
  ;
1025
};
1026
1027
$.api.settings = {
1028
1029
  name              : 'API',
1030
  namespace         : 'api',
1031
1032
  debug             : false,
1033
  verbose           : false,
1034
  performance       : true,
1035
1036
  // object containing all templates endpoints
1037
  api               : {},
1038
1039
  // whether to cache responses
1040
  cache             : true,
1041
1042
  // whether new requests should abort previous requests
1043
  interruptRequests : true,
1044
1045
  // event binding
1046
  on                : 'auto',
1047
1048
  // context for applying state classes
1049
  stateContext      : false,
1050
1051
  // duration for loading state
1052
  loadingDuration   : 0,
1053
1054
  // whether to hide errors after a period of time
1055
  hideError         : 'auto',
1056
1057
  // duration for error state
1058
  errorDuration     : 2000,
1059
1060
  // whether parameters should be encoded with encodeURIComponent
1061
  encodeParameters  : true,
1062
1063
  // API action to use
1064
  action            : false,
1065
1066
  // templated URL to use
1067
  url               : false,
1068
1069
  // base URL to apply to all endpoints
1070
  base              : '',
1071
1072
  // data that will
1073
  urlData           : {},
1074
1075
  // whether to add default data to url data
1076
  defaultData          : true,
1077
1078
  // whether to serialize closest form
1079
  serializeForm        : false,
1080
1081
  // how long to wait before request should occur
1082
  throttle             : 0,
1083
1084
  // whether to throttle first request or only repeated
1085
  throttleFirstRequest : true,
1086
1087
  // standard ajax settings
1088
  method            : 'get',
1089
  data              : {},
1090
  dataType          : 'json',
1091
1092
  // mock response
1093
  mockResponse      : false,
1094
  mockResponseAsync : false,
1095
1096
  // aliases for mock
1097
  response          : false,
1098
  responseAsync     : false,
1099
1100
  // callbacks before request
1101
  beforeSend  : function(settings) { return settings; },
1102
  beforeXHR   : function(xhr) {},
1103
  onRequest   : function(promise, xhr) {},
1104
1105
  // after request
1106
  onResponse  : false, // function(response) { },
1107
1108
  // response was successful, if JSON passed validation
1109
  onSuccess   : function(response, $module) {},
1110
1111
  // request finished without aborting
1112
  onComplete  : function(response, $module) {},
1113
1114
  // failed JSON success test
1115
  onFailure   : function(response, $module) {},
1116
1117
  // server error
1118
  onError     : function(errorMessage, $module) {},
1119
1120
  // request aborted
1121
  onAbort     : function(errorMessage, $module) {},
1122
1123
  successTest : false,
1124
1125
  // errors
1126
  error : {
1127
    beforeSend        : 'The before send function has aborted the request',
1128
    error             : 'There was an error with your request',
1129
    exitConditions    : 'API Request Aborted. Exit conditions met',
1130
    JSONParse         : 'JSON could not be parsed during error handling',
1131
    legacyParameters  : 'You are using legacy API success callback names',
1132
    method            : 'The method you called is not defined',
1133
    missingAction     : 'API action used but no url was defined',
1134
    missingSerialize  : 'jquery-serialize-object is required to add form data to an existing data object',
1135
    missingURL        : 'No URL specified for api event',
1136
    noReturnedValue   : 'The beforeSend callback must return a settings object, beforeSend ignored.',
1137
    noStorage         : 'Caching responses locally requires session storage',
1138
    parseError        : 'There was an error parsing your request',
1139
    requiredParameter : 'Missing a required URL parameter: ',
1140
    statusMessage     : 'Server gave an error: ',
1141
    timeout           : 'Your request timed out'
1142
  },
1143
1144
  regExp  : {
1145
    required : /\{\$*[A-z0-9]+\}/g,
1146
    optional : /\{\/\$*[A-z0-9]+\}/g,
1147
  },
1148
1149
  className: {
1150
    loading : 'loading',
1151
    error   : 'error'
1152
  },
1153
1154
  selector: {
1155
    disabled : '.disabled',
1156
    form      : 'form'
1157
  },
1158
1159
  metadata: {
1160
    action  : 'action',
1161
    url     : 'url'
1162
  }
1163
};
1164
1165
1166
1167
})( jQuery, window, document );
1168