UNPKG

amplifier

Version:

Use the awesome AmplifyJs v1.1.2 library as a Node.js module.

834 lines (722 loc) 28.8 kB
var jQuery = require('jquery'); /*! * Amplify 1.1.2 * * Copyright 2011 - 2013 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) { if (typeof topic !== "string") { throw new Error("You must provide a valid topic to publish."); } var args = slice.call(arguments, 1), topicSubscriptions, subscription, length, i = 0, ret; if (!subscriptions[topic]) { return true; } topicSubscriptions = subscriptions[topic].slice(); for (length = topicSubscriptions.length; i < length; i++) { subscription = topicSubscriptions[i]; ret = subscription.callback.apply(subscription.context, args); if (ret === false) { break; } } return ret !== false; }, subscribe: function (topic, context, callback, priority) { if (typeof topic !== "string") { throw new Error("You must provide a valid topic to create a subscription."); } 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, added; for (; topicIndex < topicLength; topicIndex++) { topic = topics[topicIndex]; added = false; 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); added = true; break; } } if (!added) { subscriptions[topic].unshift(subscriptionInfo); } } return callback; }, unsubscribe: function (topic, context, callback) { if (typeof topic !== "string") { throw new Error("You must provide a valid topic to remove a subscription."); } if (arguments.length === 2) { callback = context; context = null; } if (!subscriptions[topic]) { return; } var length = subscriptions[topic].length, i = 0; for (; i < length; i++) { if (subscriptions[topic][i].callback === callback) { if (!context || subscriptions[topic][i].context === context) { subscriptions[topic].splice(i, 1); // Adjust counter and length for removed item i--; length--; } } } } }; }(this)); (function (amplify, undefined) { var store = amplify.store = function (key, value, options) { 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 and Private Browsing in Safari 5 try { // Safari 5 in Private Browsing mode exposes localStorage // but doesn't allow storing data, so we attempt to store and remove an item. // This will unfortunately give us a false negative if we're at the limit. window[webStorageType].setItem("__amplify__", "x"); window[webStorageType].removeItem("__amplify__"); createFromStorageInterface(webStorageType, window[webStorageType]); } catch (e) { } } // globalStorage // non-standard: Firefox 2+ // https://developer.mozilla.org/en/dom/storage#globalStorage if (!store.types.localStorage && 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); // we can't feature detect userData support // so just try and see if it fails // surprisingly, even just adding the behavior isn't enough for a failure // so we need to load the data as well try { div.addBehavior("#default#userdata"); div.load(attrKey); } catch (e) { div.parentNode.removeChild(div); return; } 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\u037f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/g, "-"); // adjust invalid starting character to deal with our simplified sanitization key = key.replace(/^-/, "_-"); 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 = {}, timeout = {}; 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 (timeout[key]) { clearTimeout(timeout[key]); delete timeout[key]; } if (value === null) { delete memory[key]; return null; } memory[key] = value; if (options.expires) { timeout[key] = setTimeout(function () { delete memory[key]; delete timeout[key]; }, options.expires); } return value; }); }()); }(this.amplify = this.amplify || {})); (function (amplify, undefined) { 'use strict'; function noop() { } function isFunction(obj) { return ({}).toString.call(obj) === "[object Function]"; } function async(fn) { var isAsync = false; setTimeout(function () { isAsync = true; }, 1); return function () { var that = this, args = arguments; if (isAsync) { fn.apply(that, args); } else { setTimeout(function () { fn.apply(that, args); }, 1); } }; } 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 = async(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 = async(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; } }; }(this.amplify = this.amplify || {})); (function (amplify, $, undefined) { 'use strict'; 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, handleResponse, url = defnSettings.url, abort = request.abort, ajaxSettings = $.extend(true, {}, defnSettings, { data: settings.data }), 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.overrideMimeType(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); } }; handleResponse = function (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; }; amplify.publish("request.ajax.preprocess", defnSettings, settings, ajaxSettings, ampXHR); $.extend(ajaxSettings, { isJSONP: function () { return (/jsonp/gi).test(this.dataType); }, cacheURL: function () { if (!this.isJSONP()) { return this.url; } var callbackName = 'callback'; // possible for the callback function name to be overridden if (this.hasOwnProperty('jsonp')) { if (this.jsonp !== false) { callbackName = this.jsonp; } else { if (this.hasOwnProperty('jsonpCallback')) { callbackName = this.jsonpCallback; } } } // search and replace callback parameter in query string with empty string var callbackRegex = new RegExp('&?' + callbackName + '=[^&]*&?', 'gi'); return this.url.replace(callbackRegex, ''); }, 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); } }); // cache all JSONP requests if (ajaxSettings.cache && ajaxSettings.isJSONP()) { $.extend(ajaxSettings, { cache: true }); } $.ajax(ajaxSettings); request.abort = function () { ampXHR.abort(); abort.call(this); }; }; }; amplify.subscribe("request.ajax.preprocess", function (defnSettings, settings, ajaxSettings) { var mappedKeys = [], data = ajaxSettings.data; if (typeof data === "string") { return; } data = $.extend(true, {}, defnSettings.data, data); ajaxSettings.url = ajaxSettings.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]; }); ajaxSettings.data = data; }); amplify.subscribe("request.ajax.preprocess", function (defnSettings, settings, ajaxSettings) { var data = ajaxSettings.data, dataMap = defnSettings.dataMap; if (!dataMap || typeof data === "string") { return; } if ($.isFunction(dataMap)) { ajaxSettings.data = dataMap(data); } else { $.each(defnSettings.dataMap, function (orig, replace) { if (orig in data) { data[replace] = data[orig]; delete data[orig]; } }); ajaxSettings.data = data; } }); var cache = amplify.request.cache = { _key: function (resourceId, url, data) { data = url + data; var length = data.length, i = 0; /*jshint bitwise:false*/ function chunk() { return data.charCodeAt(i++) << 24 | data.charCodeAt(i++) << 16 | data.charCodeAt(i++) << 8 | data.charCodeAt(i++) << 0; } var checksum = chunk(); while (i < length) { checksum ^= chunk(); } /*jshint bitwise:true*/ 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.cacheURL(), 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.cacheURL(), 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"); } else { error(null, "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); }; }); }(this.amplify = this.amplify || {}, jQuery)); module.exports = this.amplify;