/*! * AmplifyJS 1.0.0 - Core, Store, Request * * Copyright 2011 appendTo LLC. (http://appendto.com/team) * Dual licensed under the MIT or GPL licenses. * http://appendto.com/open-source-licenses * * http://amplifyjs.com */ /*! * Amplify Core 1.0.0 * * Copyright 2011 appendTo LLC. (http://appendto.com/team) * Dual licensed under the MIT or GPL licenses. * http://appendto.com/open-source-licenses * * http://amplifyjs.com */ (function( global, undefined ) { var slice = [].slice, subscriptions = {}; var amplify = global.amplify = { publish: function( topic ) { var args = slice.call( arguments, 1 ), subscription, length, i = 0, ret; if ( !subscriptions[ topic ] ) { return true; } for ( length = subscriptions[ topic ].length; i < length; i++ ) { subscription = subscriptions[ topic ][ i ]; ret = subscription.callback.apply( subscription.context, args ); if ( ret === false ) { break; } } return ret !== false; }, subscribe: function( topic, context, callback, priority ) { if ( arguments.length === 3 && typeof callback === "number" ) { priority = callback; callback = context; context = null; } if ( arguments.length === 2 ) { callback = context; context = null; } priority = priority || 10; var topicIndex = 0, topics = topic.split( /\s/ ), topicLength = topics.length; for ( ; topicIndex < topicLength; topicIndex++ ) { topic = topics[ topicIndex ]; if ( !subscriptions[ topic ] ) { subscriptions[ topic ] = []; } var i = subscriptions[ topic ].length - 1, subscriptionInfo = { callback: callback, context: context, priority: priority }; for ( ; i >= 0; i-- ) { if ( subscriptions[ topic ][ i ].priority <= priority ) { subscriptions[ topic ].splice( i + 1, 0, subscriptionInfo ); return callback; } } subscriptions[ topic ].unshift( subscriptionInfo ); } return callback; }, unsubscribe: function( topic, callback ) { if ( !subscriptions[ topic ] ) { return; } var length = subscriptions[ topic ].length, i = 0; for ( ; i < length; i++ ) { if ( subscriptions[ topic ][ i ].callback === callback ) { subscriptions[ topic ].splice( i, 1 ); break; } } } }; }( this ) ); /*! * Amplify Store - Persistent Client-Side Storage 1.0.0 * * Copyright 2011 appendTo LLC. (http://appendto.com/team) * Dual licensed under the MIT or GPL licenses. * http://appendto.com/open-source-licenses * * http://amplifyjs.com */ (function( amplify, undefined ) { var store = amplify.store = function( key, value, options, type ) { var type = store.type; if ( options && options.type && options.type in store.types ) { type = options.type; } return store.types[ type ]( key, value, options || {} ); }; store.types = {}; store.type = null; store.addType = function( type, storage ) { if ( !store.type ) { store.type = type; } store.types[ type ] = storage; store[ type ] = function( key, value, options ) { options = options || {}; options.type = type; return store( key, value, options ); }; } store.error = function() { return "amplify.store quota exceeded"; }; var rprefix = /^__amplify__/; function createFromStorageInterface( storageType, storage ) { store.addType( storageType, function( key, value, options ) { var storedValue, parsed, i, remove, ret = value, now = (new Date()).getTime(); if ( !key ) { ret = {}; remove = []; i = 0; try { // accessing the length property works around a localStorage bug // in Firefox 4.0 where the keys don't update cross-page // we assign to key just to avoid Closure Compiler from removing // the access as "useless code" // https://bugzilla.mozilla.org/show_bug.cgi?id=662511 key = storage.length; while ( key = storage.key( i++ ) ) { if ( rprefix.test( key ) ) { parsed = JSON.parse( storage.getItem( key ) ); if ( parsed.expires && parsed.expires <= now ) { remove.push( key ); } else { ret[ key.replace( rprefix, "" ) ] = parsed.data; } } } while ( key = remove.pop() ) { storage.removeItem( key ); } } catch ( error ) {} return ret; } // protect against name collisions with direct storage key = "__amplify__" + key; if ( value === undefined ) { storedValue = storage.getItem( key ); parsed = storedValue ? JSON.parse( storedValue ) : { expires: -1 }; if ( parsed.expires && parsed.expires <= now ) { storage.removeItem( key ); } else { return parsed.data; } } else { if ( value === null ) { storage.removeItem( key ); } else { parsed = JSON.stringify({ data: value, expires: options.expires ? now + options.expires : null }); try { storage.setItem( key, parsed ); // quota exceeded } catch( error ) { // expire old data and try again store[ storageType ](); try { storage.setItem( key, parsed ); } catch( error ) { throw store.error(); } } } } return ret; }); } // localStorage + sessionStorage // IE 8+, Firefox 3.5+, Safari 4+, Chrome 4+, Opera 10.5+, iPhone 2+, Android 2+ for ( var webStorageType in { localStorage: 1, sessionStorage: 1 } ) { // try/catch for file protocol in Firefox try { if ( window[ webStorageType ].getItem ) { createFromStorageInterface( webStorageType, window[ webStorageType ] ); } } catch( e ) {} } // globalStorage // non-standard: Firefox 2+ // https://developer.mozilla.org/en/dom/storage#globalStorage if ( window.globalStorage ) { // try/catch for file protocol in Firefox try { createFromStorageInterface( "globalStorage", window.globalStorage[ window.location.hostname ] ); // Firefox 2.0 and 3.0 have sessionStorage and globalStorage // make sure we default to globalStorage // but don't default to globalStorage in 3.5+ which also has localStorage if ( store.type === "sessionStorage" ) { store.type = "globalStorage"; } } catch( e ) {} } // userData // non-standard: IE 5+ // http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx (function() { // IE 9 has quirks in userData that are a huge pain // rather than finding a way to detect these quirks // we just don't register userData if we have localStorage if ( store.types.localStorage ) { return; } // append to html instead of body so we can do this from the head var div = document.createElement( "div" ), attrKey = "amplify"; div.style.display = "none"; document.getElementsByTagName( "head" )[ 0 ].appendChild( div ); if ( div.addBehavior ) { div.addBehavior( "#default#userdata" ); store.addType( "userData", function( key, value, options ) { div.load( attrKey ); var attr, parsed, prevValue, i, remove, ret = value, now = (new Date()).getTime(); if ( !key ) { ret = {}; remove = []; i = 0; while ( attr = div.XMLDocument.documentElement.attributes[ i++ ] ) { parsed = JSON.parse( attr.value ); if ( parsed.expires && parsed.expires <= now ) { remove.push( attr.name ); } else { ret[ attr.name ] = parsed.data; } } while ( key = remove.pop() ) { div.removeAttribute( key ); } div.save( attrKey ); return ret; } // convert invalid characters to dashes // http://www.w3.org/TR/REC-xml/#NT-Name // simplified to assume the starting character is valid // also removed colon as it is invalid in HTML attribute names key = key.replace( /[^-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u37f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/g, "-" ); if ( value === undefined ) { attr = div.getAttribute( key ); parsed = attr ? JSON.parse( attr ) : { expires: -1 }; if ( parsed.expires && parsed.expires <= now ) { div.removeAttribute( key ); } else { return parsed.data; } } else { if ( value === null ) { div.removeAttribute( key ); } else { // we need to get the previous value in case we need to rollback prevValue = div.getAttribute( key ); parsed = JSON.stringify({ data: value, expires: (options.expires ? (now + options.expires) : null) }); div.setAttribute( key, parsed ); } } try { div.save( attrKey ); // quota exceeded } catch ( error ) { // roll the value back to the previous value if ( prevValue === null ) { div.removeAttribute( key ); } else { div.setAttribute( key, prevValue ); } // expire old data and try again store.userData(); try { div.setAttribute( key, parsed ); div.save( attrKey ); } catch ( error ) { // roll the value back to the previous value if ( prevValue === null ) { div.removeAttribute( key ); } else { div.setAttribute( key, prevValue ); } throw store.error(); } } return ret; }); } }() ); // in-memory storage // fallback for all browsers to enable the API even if we can't persist data (function() { var memory = {}; function copy( obj ) { return obj === undefined ? undefined : JSON.parse( JSON.stringify( obj ) ); } store.addType( "memory", function( key, value, options ) { if ( !key ) { return copy( memory ); } if ( value === undefined ) { return copy( memory[ key ] ); } if ( value === null ) { delete memory[ key ]; return null; } memory[ key ] = value; if ( options.expires ) { setTimeout(function() { delete memory[ key ]; }, options.expires ); } return value; }); }() ); }( this.amplify = this.amplify || {} ) ); /*! * Amplify Request 1.0.0 * * Copyright 2011 appendTo LLC. (http://appendto.com/team) * Dual licensed under the MIT or GPL licenses. * http://appendto.com/open-source-licenses * * http://amplifyjs.com */ (function( amplify, undefined ) { function noop() {} function isFunction( obj ) { return ({}).toString.call( obj ) === "[object Function]"; } amplify.request = function( resourceId, data, callback ) { // default to an empty hash just so we can handle a missing resourceId // in one place var settings = resourceId || {}; if ( typeof settings === "string" ) { if ( isFunction( data ) ) { callback = data; data = {}; } settings = { resourceId: resourceId, data: data || {}, success: callback }; } var request = { abort: noop }, resource = amplify.request.resources[ settings.resourceId ], success = settings.success || noop, error = settings.error || noop; settings.success = function( data, status ) { status = status || "success"; amplify.publish( "request.success", settings, data, status ); amplify.publish( "request.complete", settings, data, status ); success( data, status ); }; settings.error = function( data, status ) { status = status || "error"; amplify.publish( "request.error", settings, data, status ); amplify.publish( "request.complete", settings, data, status ); error( data, status ); }; if ( !resource ) { if ( !settings.resourceId ) { throw "amplify.request: no resourceId provided"; } throw "amplify.request: unknown resourceId: " + settings.resourceId; } if ( !amplify.publish( "request.before", settings ) ) { settings.error( null, "abort" ); return; } amplify.request.resources[ settings.resourceId ]( settings, request ); return request; }; amplify.request.types = {}; amplify.request.resources = {}; amplify.request.define = function( resourceId, type, settings ) { if ( typeof type === "string" ) { if ( !( type in amplify.request.types ) ) { throw "amplify.request.define: unknown type: " + type; } settings.resourceId = resourceId; amplify.request.resources[ resourceId ] = amplify.request.types[ type ]( settings ); } else { // no pre-processor or settings for one-off types (don't invoke) amplify.request.resources[ resourceId ] = type; } }; }( amplify ) ); (function( amplify, $, undefined ) { var xhrProps = [ "status", "statusText", "responseText", "responseXML", "readyState" ], rurlData = /\{([^\}]+)\}/g; amplify.request.types.ajax = function( defnSettings ) { defnSettings = $.extend({ type: "GET" }, defnSettings ); return function( settings, request ) { var xhr, url = defnSettings.url, data = settings.data, abort = request.abort, ajaxSettings = {}, mappedKeys = [], aborted = false, ampXHR = { readyState: 0, setRequestHeader: function( name, value ) { return xhr.setRequestHeader( name, value ); }, getAllResponseHeaders: function() { return xhr.getAllResponseHeaders(); }, getResponseHeader: function( key ) { return xhr.getResponseHeader( key ); }, overrideMimeType: function( type ) { return xhr.overrideMideType( type ); }, abort: function() { aborted = true; try { xhr.abort(); // IE 7 throws an error when trying to abort } catch( e ) {} handleResponse( null, "abort" ); }, success: function( data, status ) { settings.success( data, status ); }, error: function( data, status ) { settings.error( data, status ); } }; if ( typeof data !== "string" ) { data = $.extend( true, {}, defnSettings.data, data ); url = url.replace( rurlData, function ( m, key ) { if ( key in data ) { mappedKeys.push( key ); return data[ key ]; } }); // We delete the keys later so duplicates are still replaced $.each( mappedKeys, function ( i, key ) { delete data[ key ]; }); } $.extend( ajaxSettings, defnSettings, { url: url, type: defnSettings.type, data: data, dataType: defnSettings.dataType, success: function( data, status ) { handleResponse( data, status ); }, error: function( _xhr, status ) { handleResponse( null, status ); }, beforeSend: function( _xhr, _ajaxSettings ) { xhr = _xhr; ajaxSettings = _ajaxSettings; var ret = defnSettings.beforeSend ? defnSettings.beforeSend.call( this, ampXHR, ajaxSettings ) : true; return ret && amplify.publish( "request.before.ajax", defnSettings, settings, ajaxSettings, ampXHR ); } }); $.ajax( ajaxSettings ); function handleResponse( data, status ) { $.each( xhrProps, function( i, key ) { try { ampXHR[ key ] = xhr[ key ]; } catch( e ) {} }); // Playbook returns "HTTP/1.1 200 OK" // TODO: something also returns "OK", what? if ( /OK$/.test( ampXHR.statusText ) ) { ampXHR.statusText = "success"; } if ( data === undefined ) { // TODO: add support for ajax errors with data data = null; } if ( aborted ) { status = "abort"; } if ( /timeout|error|abort/.test( status ) ) { ampXHR.error( data, status ); } else { ampXHR.success( data, status ); } // avoid handling a response multiple times // this can happen if a request is aborted // TODO: figure out if this breaks polling or multi-part responses handleResponse = $.noop; } request.abort = function() { ampXHR.abort(); abort.call( this ); }; }; }; var cache = amplify.request.cache = { _key: function( resourceId, url, data ) { data = url + data; var length = data.length, i = 0, checksum = chunk(); while ( i < length ) { checksum ^= chunk(); } function chunk() { return data.charCodeAt( i++ ) << 24 | data.charCodeAt( i++ ) << 16 | data.charCodeAt( i++ ) << 8 | data.charCodeAt( i++ ) << 0; } return "request-" + resourceId + "-" + checksum; }, _default: (function() { var memoryStore = {}; return function( resource, settings, ajaxSettings, ampXHR ) { // data is already converted to a string by the time we get here var cacheKey = cache._key( settings.resourceId, ajaxSettings.url, ajaxSettings.data ), duration = resource.cache; if ( cacheKey in memoryStore ) { ampXHR.success( memoryStore[ cacheKey ] ); return false; } var success = ampXHR.success; ampXHR.success = function( data ) { memoryStore[ cacheKey ] = data; if ( typeof duration === "number" ) { setTimeout(function() { delete memoryStore[ cacheKey ]; }, duration ); } success.apply( this, arguments ); }; }; }()) }; if ( amplify.store ) { $.each( amplify.store.types, function( type ) { cache[ type ] = function( resource, settings, ajaxSettings, ampXHR ) { var cacheKey = cache._key( settings.resourceId, ajaxSettings.url, ajaxSettings.data ), cached = amplify.store[ type ]( cacheKey ); if ( cached ) { ajaxSettings.success( cached ); return false; } var success = ampXHR.success; ampXHR.success = function( data ) { amplify.store[ type ]( cacheKey, data, { expires: resource.cache.expires } ); success.apply( this, arguments ); }; }; }); cache.persist = cache[ amplify.store.type ]; } amplify.subscribe( "request.before.ajax", function( resource ) { var cacheType = resource.cache; if ( cacheType ) { // normalize between objects and strings/booleans/numbers cacheType = cacheType.type || cacheType; return cache[ cacheType in cache ? cacheType : "_default" ] .apply( this, arguments ); } }); amplify.request.decoders = { // http://labs.omniti.com/labs/jsend jsend: function( data, status, ampXHR, success, error ) { if ( data.status === "success" ) { success( data.data ); } else if ( data.status === "fail" ) { error( data.data, "fail" ); } else if ( data.status === "error" ) { delete data.status; error( data, "error" ); } } }; amplify.subscribe( "request.before.ajax", function( resource, settings, ajaxSettings, ampXHR ) { var _success = ampXHR.success, _error = ampXHR.error, decoder = $.isFunction( resource.decoder ) ? resource.decoder : resource.decoder in amplify.request.decoders ? amplify.request.decoders[ resource.decoder ] : amplify.request.decoders._default; if ( !decoder ) { return; } function success( data, status ) { _success( data, status ); } function error( data, status ) { _error( data, status ); } ampXHR.success = function( data, status ) { decoder( data, status, ampXHR, success, error ); }; ampXHR.error = function( data, status ) { decoder( data, status, ampXHR, success, error ); }; }); }( amplify, jQuery ) );