OSDN Git Service

fix:
[pettanr/pettanr.git] / app / assets / javascripts / backbone.fetch-cache.js
1 /*!
2   backbone.fetch-cache v1.4.1
3   by Andy Appleton - https://github.com/mrappleton/backbone-fetch-cache.git
4  */
5
6 // AMD wrapper from https://github.com/umdjs/umd/blob/master/amdWebGlobal.js
7
8 (function (root, factory) {
9   if (typeof define === 'function' && define.amd) {
10     // AMD. Register as an anonymous module and set browser global
11     define(['underscore', 'backbone', 'jquery'], function (_, Backbone, $) {
12       return (root.Backbone = factory(_, Backbone, $));
13     });
14   } else if (typeof exports !== 'undefined') {
15     module.exports = factory(require('underscore'), require('backbone'), require('jquery'));
16   } else {
17     // Browser globals
18     root.Backbone = factory(root._, root.Backbone, root.jQuery);
19   }
20 }(this, function (_, Backbone, $) {
21
22   // Setup
23   var superMethods = {
24     modelFetch: Backbone.Model.prototype.fetch,
25     modelSync: Backbone.Model.prototype.sync,
26     collectionFetch: Backbone.Collection.prototype.fetch
27   },
28   supportLocalStorage = (function() {
29     var supported = typeof window.localStorage !== 'undefined';
30     if (supported) {
31       try {
32         // impossible to write on some platforms when private browsing is on and
33         // throws an exception = local storage not supported.
34         localStorage.setItem('test_support', 'test_support');
35         localStorage.removeItem('test_support');
36       } catch (e) {
37         supported = false;
38       }
39     }
40     return supported;
41   })();
42
43   Backbone.fetchCache = (Backbone.fetchCache || {});
44   Backbone.fetchCache._cache = (Backbone.fetchCache._cache || {});
45   // Global flag to enable/disable caching
46   Backbone.fetchCache.enabled = true;
47
48   Backbone.fetchCache.priorityFn = function(a, b) {
49     if (!a || !a.expires || !b || !b.expires) {
50       return a;
51     }
52
53     return a.expires - b.expires;
54   };
55
56   Backbone.fetchCache._prioritize = function() {
57     var sorted = _.values(this._cache).sort(this.priorityFn);
58     var index = _.indexOf(_.values(this._cache), sorted[0]);
59     return _.keys(this._cache)[index];
60   };
61
62   Backbone.fetchCache._deleteCacheWithPriority = function() {
63     Backbone.fetchCache._cache[this._prioritize()] = null;
64     delete Backbone.fetchCache._cache[this._prioritize()];
65     Backbone.fetchCache.setLocalStorage();
66   };
67
68   Backbone.fetchCache.getLocalStorageKey = function() {
69     return 'backboneCache';
70   };
71
72   if (typeof Backbone.fetchCache.localStorage === 'undefined') {
73     Backbone.fetchCache.localStorage = true;
74   }
75
76   // Shared methods
77   function getCacheKey(instance, opts) {
78     var url;
79
80     if(opts && opts.url) {
81       url = opts.url;
82     } else {
83       url = _.isFunction(instance.url) ? instance.url() : instance.url;
84     }
85
86     // Need url to use as cache key so return if we can't get it
87     if(!url) { return; }
88
89     if(opts && opts.data) {
90       if(typeof opts.data === 'string') {
91         return url + '?' + opts.data;
92       } else {
93         return url + '?' + $.param(opts.data);
94       }
95     }
96     return url;
97   }
98
99   function setCache(instance, opts, attrs) {
100     opts = (opts || {});
101     var key = Backbone.fetchCache.getCacheKey(instance, opts),
102         expires = false;
103
104     // Need url to use as cache key so return if we can't get it
105     if (!key) { return; }
106
107     // Never set the cache if user has explicitly said not to
108     if (opts.cache === false) { return; }
109
110     // Don't set the cache unless cache: true or prefill: true option is passed
111     if (!(opts.cache || opts.prefill)) { return; }
112
113     if (opts.expires !== false) {
114       expires = (new Date()).getTime() + ((opts.expires || 5 * 60) * 1000);
115     }
116
117     Backbone.fetchCache._cache[key] = {
118       expires: expires,
119       value: attrs
120     };
121
122     Backbone.fetchCache.setLocalStorage();
123   }
124
125   function clearItem(key) {
126     if (_.isFunction(key)) { key = key(); }
127     delete Backbone.fetchCache._cache[key];
128     Backbone.fetchCache.setLocalStorage();
129   }
130
131   function setLocalStorage() {
132     if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
133     try {
134       localStorage.setItem(Backbone.fetchCache.getLocalStorageKey(), JSON.stringify(Backbone.fetchCache._cache));
135     } catch (err) {
136       var code = err.code || err.number || err.message;
137       if (code === 22) {
138         this._deleteCacheWithPriority();
139       } else {
140         throw(err);
141       }
142     }
143   }
144
145   function getLocalStorage() {
146     if (!supportLocalStorage || !Backbone.fetchCache.localStorage) { return; }
147     var json = localStorage.getItem(Backbone.fetchCache.getLocalStorageKey()) || '{}';
148     Backbone.fetchCache._cache = JSON.parse(json);
149   }
150
151   function nextTick(fn) {
152     return window.setTimeout(fn, 0);
153   }
154
155   // Instance methods
156   Backbone.Model.prototype.fetch = function(opts) {
157     //Bypass caching if it's not enabled
158     if(!Backbone.fetchCache.enabled) {
159       return superMethods.modelFetch.apply(this, arguments);
160     }
161     opts = _.defaults(opts || {}, { parse: true });
162     var key = Backbone.fetchCache.getCacheKey(this, opts),
163         data = Backbone.fetchCache._cache[key],
164         expired = false,
165         attributes = false,
166         deferred = new $.Deferred(),
167         self = this;
168
169     function setData() {
170       if (opts.parse) {
171         attributes = self.parse(attributes, opts);
172       }
173
174       self.set(attributes, opts);
175       if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self, attributes, opts); }
176
177       // Trigger sync events
178       self.trigger('cachesync', self, attributes, opts);
179       self.trigger('sync', self, attributes, opts);
180
181       // Notify progress if we're still waiting for an AJAX call to happen...
182       if (opts.prefill) { deferred.notify(self); }
183       // ...finish and return if we're not
184       else {
185         if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
186         deferred.resolve(self);
187       }
188     }
189
190     if (data) {
191       expired = data.expires;
192       expired = expired && data.expires < (new Date()).getTime();
193       attributes = data.value;
194     }
195
196     if (!expired && (opts.cache || opts.prefill) && attributes) {
197       // Ensure that cache resolution adhers to async option, defaults to true.
198       if (opts.async == null) { opts.async = true; }
199
200       if (opts.async) {
201         nextTick(setData);
202       } else {
203         setData();
204       }
205
206       if (!opts.prefill) {
207         return deferred;
208       }
209     }
210
211     // Delegate to the actual fetch method and store the attributes in the cache
212     var jqXHR = superMethods.modelFetch.apply(this, arguments);
213     // resolve the returned promise when the AJAX call completes
214     jqXHR.done( _.bind(deferred.resolve, this, this) )
215       // Set the new data in the cache
216       .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) )
217       // Reject the promise on fail
218       .fail( _.bind(deferred.reject, this, this) );
219
220     deferred.abort = jqXHR.abort;
221
222     // return a promise which provides the same methods as a jqXHR object
223     return deferred;
224   };
225
226   // Override Model.prototype.sync and try to clear cache items if it looks
227   // like they are being updated.
228   Backbone.Model.prototype.sync = function(method, model, options) {
229     // Only empty the cache if we're doing a create, update, patch or delete.
230     // or caching is not enabled
231     if (method === 'read' || !Backbone.fetchCache.enabled) {
232       return superMethods.modelSync.apply(this, arguments);
233     }
234
235     var collection = model.collection,
236         keys = [],
237         i, len;
238
239     // Build up a list of keys to delete from the cache, starting with this
240     keys.push(Backbone.fetchCache.getCacheKey(model, options));
241
242     // If this model has a collection, also try to delete the cache for that
243     if (!!collection) {
244       keys.push(Backbone.fetchCache.getCacheKey(collection));
245     }
246
247     // Empty cache for all found keys
248     for (i = 0, len = keys.length; i < len; i++) { clearItem(keys[i]); }
249
250     return superMethods.modelSync.apply(this, arguments);
251   };
252
253   Backbone.Collection.prototype.fetch = function(opts) {
254     // Bypass caching if it's not enabled
255     if(!Backbone.fetchCache.enabled) {
256       return superMethods.collectionFetch.apply(this, arguments);
257     }
258
259     opts = _.defaults(opts || {}, { parse: true });
260     var key = Backbone.fetchCache.getCacheKey(this, opts),
261         data = Backbone.fetchCache._cache[key],
262         expired = false,
263         attributes = false,
264         deferred = new $.Deferred(),
265         self = this;
266
267     function setData() {
268       self[opts.reset ? 'reset' : 'set'](attributes, opts);
269       if (_.isFunction(opts.prefillSuccess)) { opts.prefillSuccess(self); }
270
271       // Trigger sync events
272       self.trigger('cachesync', self, attributes, opts);
273       self.trigger('sync', self, attributes, opts);
274
275       // Notify progress if we're still waiting for an AJAX call to happen...
276       if (opts.prefill) { deferred.notify(self); }
277       // ...finish and return if we're not
278       else {
279         if (_.isFunction(opts.success)) { opts.success(self, attributes, opts); }
280         deferred.resolve(self);
281       }
282     }
283
284     if (data) {
285       expired = data.expires;
286       expired = expired && data.expires < (new Date()).getTime();
287       attributes = data.value;
288     }
289
290     if (!expired && (opts.cache || opts.prefill) && attributes) {
291       // Ensure that cache resolution adhers to async option, defaults to true.
292       if (opts.async == null) { opts.async = true; }
293
294       if (opts.async) {
295         nextTick(setData);
296       } else {
297         setData();
298       }
299
300       if (!opts.prefill) {
301         return deferred;
302       }
303     }
304
305     // Delegate to the actual fetch method and store the attributes in the cache
306     var jqXHR = superMethods.collectionFetch.apply(this, arguments);
307     // resolve the returned promise when the AJAX call completes
308     jqXHR.done( _.bind(deferred.resolve, this, this) )
309       // Set the new data in the cache
310       .done( _.bind(Backbone.fetchCache.setCache, null, this, opts) )
311       // Reject the promise on fail
312       .fail( _.bind(deferred.reject, this, this) );
313
314     deferred.abort = jqXHR.abort;
315
316     // return a promise which provides the same methods as a jqXHR object
317     return deferred;
318   };
319
320   // Prime the cache from localStorage on initialization
321   getLocalStorage();
322
323   // Exports
324
325   Backbone.fetchCache._superMethods = superMethods;
326   Backbone.fetchCache.setCache = setCache;
327   Backbone.fetchCache.getCacheKey = getCacheKey;
328   Backbone.fetchCache.clearItem = clearItem;
329   Backbone.fetchCache.setLocalStorage = setLocalStorage;
330   Backbone.fetchCache.getLocalStorage = getLocalStorage;
331
332   return Backbone;
333 }));