; (function ($, window, document, undefined){ 'use strict'; var window = (typeof window != 'undefined' && window.Math == Math)? window: (typeof self != 'undefined' && self.Math == Math)? self: Function('return this')(); $.api = $.fn.api = function (parameters){ var $allModules = $.isFunction(this)? $(window): $(this), moduleSelector = $allModules.selector || '', time = new Date().getTime(), performance = [] , query = arguments[0], methodInvoked = (typeof query == 'string'), queryArguments = [] .slice.call(arguments, 1), returnedValue; $allModules.each(function (){ var settings = ($.isPlainObject(parameters))? $.extend(true , { } , $.fn.api.settings, parameters): $.extend({ } , $.fn.api.settings), namespace = settings.namespace, metadata = settings.metadata, selector = settings.selector, error = settings.error, className = settings.className, eventNamespace = '.' + namespace, moduleNamespace = 'module-' + namespace, $module = $(this), $form = $module.closest(selector.form), $context = (settings.stateContext)? $(settings.stateContext): $module, ajaxSettings, requestSettings, url, data, requestStartTime, element = this, context = $context[0], instance = $module.data(moduleNamespace), module; module = { initialize: function (){ if (!methodInvoked) { module.bind.events(); } module.instantiate(); } , instantiate: function (){ module.verbose('Storing instance of module', module); instance = module; $module.data(moduleNamespace, instance); } , destroy: function (){ module.verbose('Destroying previous module for', element); $module.removeData(moduleNamespace).off(eventNamespace); } , bind: { events: function (){ var triggerEvent = module.get.event(); if (triggerEvent) { module.verbose('Attaching API events to element', triggerEvent); $module.on(triggerEvent + eventNamespace, module.event.trigger); } else if (settings.on == 'now') { module.debug('Querying API endpoint immediately'); module.query(); } } } , decode: { json: function (response){ if (response !== undefined && typeof response == 'string') { try { response = JSON.parse(response); } catch (e) { } } return response; } } , read: { cachedResponse: function (url){ var response; if (window.Storage === undefined) { module.error(error.noStorage); return ; } response = _AN_Call_getitem('getItem', sessionStorage, url); module.debug('Using cached response', url, response); response = module.decode.json(response); return response; } } , write: { cachedResponse: function (url, response){ if (response && response === '') { module.debug('Response empty, not caching', response); return ; } if (window.Storage === undefined) { module.error(error.noStorage); return ; } if ($.isPlainObject(response)) { response = JSON.stringify(response); } _AN_Call_setitem('setItem', sessionStorage, url, response); module.verbose('Storing cached response for url', url, response); } } , query: function (){ if (module.is.disabled()) { module.debug('Element is disabled API request aborted'); return ; } if (module.is.loading()) { if (settings.interruptRequests) { module.debug('Interrupting previous request'); module.abort(); } else { module.debug('Cancelling request, previous request is still pending'); return ; } } if (settings.defaultData) { $.extend(true , settings.urlData, module.get.defaultData()); } if (settings.serializeForm) { settings.data = module.add.formData(settings.data); } requestSettings = module.get.settings(); if (requestSettings === false ) { module.cancelled = true ; module.error(error.beforeSend); return ; } else { module.cancelled = false ; } url = module.get.templatedURL(); if (!url && !module.is.mocked()) { module.error(error.missingURL); return ; } url = module.add.urlData(url); if (!url && !module.is.mocked()) { return ; } _AN_Write_url('url', requestSettings, false , settings.base + url); ajaxSettings = $.extend(true , { } , settings, { type: settings.method || settings.type, data: data, url: settings.base + url, beforeSend: settings.beforeXHR, success: function (){ } , failure: function (){ } , complete: function (){ } } ); module.debug('Querying URL', _AN_Read_url('url', ajaxSettings)); module.verbose('Using AJAX settings', ajaxSettings); if (settings.cache === 'local' && module.read.cachedResponse(url)) { module.debug('Response returned from local cache'); module.request = module.create.request(); module.request.resolveWith(context, [module.read.cachedResponse(url)] ); return ; } if (!settings.throttle) { module.debug('Sending request', data, ajaxSettings.method); module.send.request(); } else { if (!settings.throttleFirstRequest && !module.timer) { module.debug('Sending request', data, ajaxSettings.method); module.send.request(); module.timer = _AN_Call_settimeout('setTimeout', window, function (){ } , settings.throttle); } else { module.debug('Throttling request', settings.throttle); clearTimeout(module.timer); module.timer = _AN_Call_settimeout('setTimeout', window, function (){ if (module.timer) { delete module.timer; } module.debug('Sending throttled request', data, ajaxSettings.method); module.send.request(); } , settings.throttle); } } } , should: { removeError: function (){ return (settings.hideError === true || (settings.hideError === 'auto' && !module.is.form())); } } , is: { disabled: function (){ return (_AN_Read_length('length', $module.filter(selector.disabled)) > 0); } , expectingJSON: function (){ return settings.dataType === 'json' || settings.dataType === 'jsonp'; } , form: function (){ return $module.is('form') || $context.is('form'); } , mocked: function (){ return (settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync); } , input: function (){ return $module.is('input'); } , loading: function (){ return (module.request)? (module.request.state() == 'pending'): false ; } , abortedRequest: function (xhr){ if (xhr && xhr.readyState !== undefined && xhr.readyState === 0) { module.verbose('XHR request determined to be aborted'); return true ; } else { module.verbose('XHR request was not aborted'); return false ; } } , validResponse: function (response){ if ((!module.is.expectingJSON()) || !$.isFunction(settings.successTest)) { module.verbose('Response is not JSON, skipping validation', settings.successTest, response); return true ; } module.debug('Checking JSON returned success', settings.successTest, response); if (settings.successTest(response)) { module.debug('Response passed success test', response); return true ; } else { module.debug('Response failed success test', response); return false ; } } } , was: { cancelled: function (){ return (module.cancelled || false ); } , succesful: function (){ return (module.request && module.request.state() == 'resolved'); } , failure: function (){ return (module.request && module.request.state() == 'rejected'); } , complete: function (){ return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected')); } } , add: { urlData: function (url, urlData){ var requiredVariables, optionalVariables; if (url) { requiredVariables = url.match(settings.regExp.required); optionalVariables = url.match(settings.regExp.optional); urlData = urlData || settings.urlData; if (requiredVariables) { module.debug('Looking for required URL variables', requiredVariables); $.each(requiredVariables, function (index, templatedString){ var variable = (templatedString.indexOf('$') !== -1)? templatedString.substr(2, _AN_Read_length('length', templatedString) - 3): templatedString.substr(1, _AN_Read_length('length', templatedString) - 2), value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)? urlData[variable]: ($module.data(variable) !== undefined)? $module.data(variable): ($context.data(variable) !== undefined)? $context.data(variable): urlData[variable]; if (value === undefined) { module.error(error.requiredParameter, variable, url); url = false ; return false ; } else { module.verbose('Found required variable', variable, value); value = (settings.encodeParameters)? module.get.urlEncodedValue(value): value; url = _AN_Call_replace('replace', url, templatedString, value); } } ); } if (optionalVariables) { module.debug('Looking for optional URL variables', requiredVariables); $.each(optionalVariables, function (index, templatedString){ var variable = (templatedString.indexOf('$') !== -1)? templatedString.substr(3, _AN_Read_length('length', templatedString) - 4): templatedString.substr(2, _AN_Read_length('length', templatedString) - 3), value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)? urlData[variable]: ($module.data(variable) !== undefined)? $module.data(variable): ($context.data(variable) !== undefined)? $context.data(variable): urlData[variable]; if (value !== undefined) { module.verbose('Optional variable Found', variable, value); url = _AN_Call_replace('replace', url, templatedString, value); } else { module.verbose('Optional variable not found', variable); if (url.indexOf('/' + templatedString) !== -1) { url = _AN_Call_replace('replace', url, '/' + templatedString, ''); } else { url = _AN_Call_replace('replace', url, templatedString, ''); } } } ); } } return url; } , formData: function (data){ var canSerialize = ($.fn.serializeObject !== undefined), formData = (canSerialize)? $form.serializeObject(): $form.serialize(), hasOtherData; data = data || settings.data; hasOtherData = $.isPlainObject(data); if (hasOtherData) { if (canSerialize) { module.debug('Extending existing data with form data', data, formData); data = $.extend(true , { } , data, formData); } else { module.error(error.missingSerialize); module.debug('Cant extend data. Replacing data with form data', data, formData); data = formData; } } else { module.debug('Adding form data', formData); data = formData; } return data; } } , send: { request: function (){ module.set.loading(); module.request = module.create.request(); if (module.is.mocked()) { module.mockedXHR = module.create.mockedXHR(); } else { module.xhr = module.create.xhr(); } settings.onRequest.call(context, module.request, module.xhr); } } , event: { trigger: function (event){ module.query(); if (event.type == 'submit' || event.type == 'click') { event.preventDefault(); } } , xhr: { always: function (){ } , done: function (response, textStatus, xhr){ var context = this, elapsedTime = (new Date().getTime() - requestStartTime), timeLeft = (settings.loadingDuration - elapsedTime), translatedResponse = ($.isFunction(settings.onResponse))? module.is.expectingJSON()? settings.onResponse.call(context, $.extend(true , { } , response)): settings.onResponse.call(context, response): false ; timeLeft = (timeLeft > 0)? timeLeft: 0; if (translatedResponse) { module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response); response = translatedResponse; } if (timeLeft > 0) { module.debug('Response completed early delaying state change by', timeLeft); } _AN_Call_settimeout('setTimeout', window, function (){ if (module.is.validResponse(response)) { module.request.resolveWith(context, [response, xhr] ); } else { module.request.rejectWith(context, [xhr, 'invalid'] ); } } , timeLeft); } , fail: function (xhr, status, httpMessage){ var context = this, elapsedTime = (new Date().getTime() - requestStartTime), timeLeft = (settings.loadingDuration - elapsedTime); timeLeft = (timeLeft > 0)? timeLeft: 0; if (timeLeft > 0) { module.debug('Response completed early delaying state change by', timeLeft); } _AN_Call_settimeout('setTimeout', window, function (){ if (module.is.abortedRequest(xhr)) { module.request.rejectWith(context, [xhr, 'aborted', httpMessage] ); } else { module.request.rejectWith(context, [xhr, 'error', status, httpMessage] ); } } , timeLeft); } } , request: { done: function (response, xhr){ module.debug('Successful API Response', response); if (settings.cache === 'local' && url) { module.write.cachedResponse(url, response); module.debug('Saving server response locally', module.cache); } settings.onSuccess.call(context, response, $module, xhr); } , complete: function (firstParameter, secondParameter){ var xhr, response; if (module.was.succesful()) { response = firstParameter; xhr = secondParameter; } else { xhr = firstParameter; response = module.get.responseFromXHR(xhr); } module.remove.loading(); settings.onComplete.call(context, response, $module, xhr); } , fail: function (xhr, status, httpMessage){ var response = module.get.responseFromXHR(xhr), errorMessage = module.get.errorFromRequest(response, status, httpMessage); if (status == 'aborted') { module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage); settings.onAbort.call(context, status, $module, xhr); return true ; } else if (status == 'invalid') { module.debug('JSON did not pass success test. A server-side error has most likely occurred', response); } else if (status == 'error') { if (xhr !== undefined) { module.debug('XHR produced a server error', status, httpMessage); if (xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') { module.error(error.statusMessage + httpMessage, _AN_Read_url('url', ajaxSettings)); } settings.onError.call(context, errorMessage, $module, xhr); } } if (settings.errorDuration && status !== 'aborted') { module.debug('Adding error state'); module.set.error(); if (module.should.removeError()) { _AN_Call_settimeout('setTimeout', window, module.remove.error, settings.errorDuration); } } module.debug('API Request failed', errorMessage, xhr); settings.onFailure.call(context, response, $module, xhr); } } } , create: { request: function (){ return $.Deferred().always(module.event.request.complete).done(module.event.request.done).fail(module.event.request.fail); } , mockedXHR: function (){ var textStatus = false , status = false , httpMessage = false , responder = settings.mockResponse || settings.response, asyncResponder = settings.mockResponseAsync || settings.responseAsync, asyncCallback, response, mockedXHR; mockedXHR = $.Deferred().always(module.event.xhr.complete).done(module.event.xhr.done).fail(module.event.xhr.fail); if (responder) { if ($.isFunction(responder)) { module.debug('Using specified synchronous callback', responder); response = responder.call(context, requestSettings); } else { module.debug('Using settings specified response', responder); response = responder; } mockedXHR.resolveWith(context, [response, textStatus, { responseText: response} ] ); } else if ($.isFunction(asyncResponder)) { asyncCallback = function (response){ module.debug('Async callback returned response', response); if (response) { mockedXHR.resolveWith(context, [response, textStatus, { responseText: response} ] ); } else { mockedXHR.rejectWith(context, [{ responseText: response} , status, httpMessage] ); } } ; module.debug('Using specified async response callback', asyncResponder); asyncResponder.call(context, requestSettings, asyncCallback); } return mockedXHR; } , xhr: function (){ var xhr; xhr = $.ajax(ajaxSettings).always(module.event.xhr.always).done(module.event.xhr.done).fail(module.event.xhr.fail); module.verbose('Created server request', xhr, ajaxSettings); return xhr; } } , set: { error: function (){ module.verbose('Adding error state to element', $context); $context.addClass(className.error); } , loading: function (){ module.verbose('Adding loading state to element', $context); $context.addClass(className.loading); requestStartTime = new Date().getTime(); } } , remove: { error: function (){ module.verbose('Removing error state from element', $context); $context.removeClass(className.error); } , loading: function (){ module.verbose('Removing loading state from element', $context); $context.removeClass(className.loading); } } , get: { responseFromXHR: function (xhr){ return $.isPlainObject(xhr)? (module.is.expectingJSON())? module.decode.json(xhr.responseText): xhr.responseText: false ; } , errorFromRequest: function (response, status, httpMessage){ return ($.isPlainObject(response) && response.error !== undefined)? response.error: (settings.error[status] !== undefined)? settings.error[status]: httpMessage; } , request: function (){ return module.request || false ; } , xhr: function (){ return module.xhr || false ; } , settings: function (){ var runSettings; runSettings = settings.beforeSend.call(context, settings); if (runSettings) { if (runSettings.success !== undefined) { module.debug('Legacy success callback detected', runSettings); module.error(error.legacyParameters, runSettings.success); runSettings.onSuccess = runSettings.success; } if (runSettings.failure !== undefined) { module.debug('Legacy failure callback detected', runSettings); module.error(error.legacyParameters, runSettings.failure); runSettings.onFailure = runSettings.failure; } if (runSettings.complete !== undefined) { module.debug('Legacy complete callback detected', runSettings); module.error(error.legacyParameters, runSettings.complete); runSettings.onComplete = runSettings.complete; } } if (runSettings === undefined) { module.error(error.noReturnedValue); } if (runSettings === false ) { return runSettings; } return (runSettings !== undefined)? $.extend(true , { } , runSettings): $.extend(true , { } , settings); } , urlEncodedValue: function (value){ var decodedValue = window.decodeURIComponent(value), encodedValue = window.encodeURIComponent(value), alreadyEncoded = (decodedValue !== value); if (alreadyEncoded) { module.debug('URL value is already encoded, avoiding double encoding', value); return value; } module.verbose('Encoding value using encodeURIComponent', value, encodedValue); return encodedValue; } , defaultData: function (){ var data = { } ; if (!$.isWindow(element)) { if (module.is.input()) { data.value = $module.val(); } else if (module.is.form()) { } else { _AN_Write_text('text', data, false , $module.text()); } } return data; } , event: function (){ if ($.isWindow(element) || settings.on == 'now') { module.debug('API called without element, no events attached'); return false ; } else if (settings.on == 'auto') { if ($module.is('input')) { return (element.oninput !== undefined)? 'input': (element.onpropertychange !== undefined)? 'propertychange': 'keyup'; } else if ($module.is('form')) { return 'submit'; } else { return 'click'; } } else { return settings.on; } } , templatedURL: function (action){ action = action || $module.data(_AN_Read_action('action', metadata)) || _AN_Read_action('action', settings) || false ; url = $module.data(_AN_Read_url('url', metadata)) || _AN_Read_url('url', settings) || false ; if (url) { module.debug('Using specified url', url); return url; } if (action) { module.debug('Looking up url for action', action, settings.api); if (settings.api[action] === undefined && !module.is.mocked()) { module.error(error.missingAction, _AN_Read_action('action', settings), settings.api); return ; } url = settings.api[action]; } else if (module.is.form()) { url = $module.attr('action') || $context.attr('action') || false ; module.debug('No url or action specified, defaulting to form action', url); } return url; } } , abort: function (){ var xhr = module.get.xhr(); if (xhr && xhr.state() !== 'resolved') { module.debug('Cancelling API request'); xhr.abort(); } } , reset: function (){ module.remove.error(); module.remove.loading(); } , setting: function (name, value){ module.debug('Changing setting', name, value); if ($.isPlainObject(name)) { $.extend(true , settings, name); } else if (value !== undefined) { if ($.isPlainObject(settings[name])) { $.extend(true , settings[name], value); } else { settings[name] = value; } } else { return settings[name]; } } , internal: function (name, value){ if ($.isPlainObject(name)) { $.extend(true , module, name); } else if (value !== undefined) { module[name] = value; } else { return module[name]; } } , debug: function (){ if (!settings.silent && settings.debug) { if (settings.performance) { module.performance.log(arguments); } else { module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); module.debug.apply(console, arguments); } } } , verbose: function (){ if (!settings.silent && settings.verbose && settings.debug) { if (settings.performance) { module.performance.log(arguments); } else { module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); module.verbose.apply(console, arguments); } } } , error: function (){ if (!settings.silent) { module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); module.error.apply(console, arguments); } } , performance: { log: function (message){ var currentTime, executionTime, previousTime; if (settings.performance) { currentTime = new Date().getTime(); previousTime = time || currentTime; executionTime = currentTime - previousTime; time = currentTime; performance.push({ 'Name': message[0], 'Arguments': [] .slice.call(message, 1) || '', 'Execution Time': executionTime} ); } clearTimeout(module.performance.timer); module.performance.timer = _AN_Call_settimeout('setTimeout', window, module.performance.display, 500); } , display: function (){ var title = settings.name + ':', totalTime = 0; time = false ; clearTimeout(module.performance.timer); $.each(performance, function (index, data){ totalTime += data["Execution Time"] ; } ); title += ' ' + totalTime + 'ms'; if (moduleSelector) { title += ' \'' + moduleSelector + '\''; } if ((console.group !== undefined || console.table !== undefined) && _AN_Read_length('length', performance) > 0) { console.groupCollapsed(title); if (console.table) { console.table(performance); } else { $.each(performance, function (index, data){ console.log(data.Name + ': ' + data["Execution Time"] + 'ms'); } ); } console.groupEnd(); } performance = [] ; } } , invoke: function (query, passedArguments, context){ var object = instance, maxDepth, found, response; passedArguments = passedArguments || queryArguments; context = element || context; if (typeof query == 'string' && object !== undefined) { query = query.split(/[\. ]/); maxDepth = _AN_Read_length('length', query) - 1; $.each(query, function (depth, value){ var camelCaseValue = (depth != maxDepth)? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1): query; if ($.isPlainObject(object[camelCaseValue]) && (depth != maxDepth)) { object = object[camelCaseValue]; } else if (object[camelCaseValue] !== undefined) { found = object[camelCaseValue]; return false ; } else if ($.isPlainObject(object[value]) && (depth != maxDepth)) { object = object[value]; } else if (object[value] !== undefined) { found = object[value]; return false ; } else { module.error(error.method, query); return false ; } } ); } if ($.isFunction(found)) { response = found.apply(context, passedArguments); } else if (found !== undefined) { response = found; } if ($.isArray(returnedValue)) { returnedValue.push(response); } else if (returnedValue !== undefined) { returnedValue = [returnedValue, response] ; } else if (response !== undefined) { returnedValue = response; } return found; } } ; if (methodInvoked) { if (instance === undefined) { module.initialize(); } module.invoke(query); } else { if (instance !== undefined) { instance.invoke('destroy'); } module.initialize(); } } ); return (returnedValue !== undefined)? returnedValue: this; } ; $.api.settings = { name: 'API', namespace: 'api', debug: false , verbose: false , performance: true , api: { } , cache: true , interruptRequests: true , on: 'auto', stateContext: false , loadingDuration: 0, hideError: 'auto', errorDuration: 2000, encodeParameters: true , action: false , url: false , base: '', urlData: { } , defaultData: true , serializeForm: false , throttle: 0, throttleFirstRequest: true , method: 'get', data: { } , dataType: 'json', mockResponse: false , mockResponseAsync: false , response: false , responseAsync: false , beforeSend: function (settings){ return settings; } , beforeXHR: function (xhr){ } , onRequest: function (promise, xhr){ } , onResponse: false , onSuccess: function (response, $module){ } , onComplete: function (response, $module){ } , onFailure: function (response, $module){ } , onError: function (errorMessage, $module){ } , onAbort: function (errorMessage, $module){ } , successTest: false , error: { beforeSend: 'The before send function has aborted the request', error: 'There was an error with your request', exitConditions: 'API Request Aborted. Exit conditions met', JSONParse: 'JSON could not be parsed during error handling', legacyParameters: 'You are using legacy API success callback names', method: 'The method you called is not defined', missingAction: 'API action used but no url was defined', missingSerialize: 'jquery-serialize-object is required to add form data to an existing data object', missingURL: 'No URL specified for api event', noReturnedValue: 'The beforeSend callback must return a settings object, beforeSend ignored.', noStorage: 'Caching responses locally requires session storage', parseError: 'There was an error parsing your request', requiredParameter: 'Missing a required URL parameter: ', statusMessage: 'Server gave an error: ', timeout: 'Your request timed out'} , regExp: { required: /\{\$*[A-z0-9]+\}/g, optional: /\{\/\$*[A-z0-9]+\}/g} , className: { loading: 'loading', error: 'error'} , selector: { disabled: '.disabled', form: 'form'} , metadata: { action: 'action', url: 'url'} } ; } )(jQuery, window, document);