]>
Commit | Line | Data |
---|---|---|
1354d172 AD |
1 | define("dojo/store/Observable", ["../_base/kernel", "../_base/lang", "../_base/Deferred", "../_base/array" |
2 | ], function(kernel, lang, Deferred, array) { | |
3 | // module: | |
4 | // dojo/store/Observable | |
5 | // summary: | |
6 | // TODOC | |
7 | ||
8 | var ds = lang.getObject("dojo.store", true); | |
9 | ||
10 | return ds.Observable = function(store){ | |
11 | // summary: | |
12 | // The Observable store wrapper takes a store and sets an observe method on query() | |
13 | // results that can be used to monitor results for changes. | |
14 | // | |
15 | // description: | |
16 | // Observable wraps an existing store so that notifications can be made when a query | |
17 | // is performed. | |
18 | // | |
19 | // example: | |
20 | // Create a Memory store that returns an observable query, and then log some | |
21 | // information about that query. | |
22 | // | |
23 | // | var store = dojo.store.Observable(new dojo.store.Memory({ | |
24 | // | data: [ | |
25 | // | {id: 1, name: "one", prime: false}, | |
26 | // | {id: 2, name: "two", even: true, prime: true}, | |
27 | // | {id: 3, name: "three", prime: true}, | |
28 | // | {id: 4, name: "four", even: true, prime: false}, | |
29 | // | {id: 5, name: "five", prime: true} | |
30 | // | ] | |
31 | // | })); | |
32 | // | var changes = [], results = store.query({ prime: true }); | |
33 | // | var observer = results.observe(function(object, previousIndex, newIndex){ | |
34 | // | changes.push({previousIndex:previousIndex, newIndex:newIndex, object:object}); | |
35 | // | }); | |
36 | // | |
37 | // See the Observable tests for more information. | |
38 | ||
39 | var undef, queryUpdaters = [], revision = 0; | |
40 | // a Comet driven store could directly call notify to notify observers when data has | |
41 | // changed on the backend | |
42 | store.notify = function(object, existingId){ | |
43 | revision++; | |
44 | var updaters = queryUpdaters.slice(); | |
45 | for(var i = 0, l = updaters.length; i < l; i++){ | |
46 | updaters[i](object, existingId); | |
47 | } | |
48 | }; | |
49 | var originalQuery = store.query; | |
50 | store.query = function(query, options){ | |
51 | options = options || {}; | |
52 | var results = originalQuery.apply(this, arguments); | |
53 | if(results && results.forEach){ | |
54 | var nonPagedOptions = lang.mixin({}, options); | |
55 | delete nonPagedOptions.start; | |
56 | delete nonPagedOptions.count; | |
57 | ||
58 | var queryExecutor = store.queryEngine && store.queryEngine(query, nonPagedOptions); | |
59 | var queryRevision = revision; | |
60 | var listeners = [], queryUpdater; | |
61 | results.observe = function(listener, includeObjectUpdates){ | |
62 | if(listeners.push(listener) == 1){ | |
63 | // first listener was added, create the query checker and updater | |
64 | queryUpdaters.push(queryUpdater = function(changed, existingId){ | |
65 | Deferred.when(results, function(resultsArray){ | |
66 | var atEnd = resultsArray.length != options.count; | |
67 | var i, l, listener; | |
68 | if(++queryRevision != revision){ | |
69 | throw new Error("Query is out of date, you must observe() the query prior to any data modifications"); | |
70 | } | |
71 | var removedObject, removedFrom = -1, insertedInto = -1; | |
72 | if(existingId !== undef){ | |
73 | // remove the old one | |
74 | for(i = 0, l = resultsArray.length; i < l; i++){ | |
75 | var object = resultsArray[i]; | |
76 | if(store.getIdentity(object) == existingId){ | |
77 | removedObject = object; | |
78 | removedFrom = i; | |
79 | if(queryExecutor || !changed){// if it was changed and we don't have a queryExecutor, we shouldn't remove it because updated objects would be eliminated | |
80 | resultsArray.splice(i, 1); | |
81 | } | |
82 | break; | |
83 | } | |
84 | } | |
85 | } | |
86 | if(queryExecutor){ | |
87 | // add the new one | |
88 | if(changed && | |
89 | // if a matches function exists, use that (probably more efficient) | |
90 | (queryExecutor.matches ? queryExecutor.matches(changed) : queryExecutor([changed]).length)){ | |
91 | ||
92 | var firstInsertedInto = removedFrom > -1 ? | |
93 | removedFrom : // put back in the original slot so it doesn't move unless it needs to (relying on a stable sort below) | |
94 | resultsArray.length; | |
95 | resultsArray.splice(firstInsertedInto, 0, changed); // add the new item | |
96 | insertedInto = array.indexOf(queryExecutor(resultsArray), changed); // sort it | |
97 | // we now need to push the chagne back into the original results array | |
98 | resultsArray.splice(firstInsertedInto, 1); // remove the inserted item from the previous index | |
99 | ||
100 | if((options.start && insertedInto == 0) || | |
101 | (!atEnd && insertedInto == resultsArray.length)){ | |
102 | // if it is at the end of the page, assume it goes into the prev or next page | |
103 | insertedInto = -1; | |
104 | }else{ | |
105 | resultsArray.splice(insertedInto, 0, changed); // and insert into the results array with the correct index | |
106 | } | |
107 | } | |
108 | }else if(changed && !options.start){ | |
109 | // we don't have a queryEngine, so we can't provide any information | |
110 | // about where it was inserted, but we can at least indicate a new object | |
111 | insertedInto = removedFrom >= 0 ? removedFrom : (store.defaultIndex || 0); | |
112 | } | |
113 | if((removedFrom > -1 || insertedInto > -1) && | |
114 | (includeObjectUpdates || !queryExecutor || (removedFrom != insertedInto))){ | |
115 | var copyListeners = listeners.slice(); | |
116 | for(i = 0;listener = copyListeners[i]; i++){ | |
117 | listener(changed || removedObject, removedFrom, insertedInto); | |
118 | } | |
119 | } | |
120 | }); | |
121 | }); | |
122 | } | |
123 | return { | |
124 | cancel: function(){ | |
125 | // remove this listener | |
126 | var index = array.indexOf(listeners, listener); | |
127 | if(index > -1){ // check to make sure we haven't already called cancel | |
128 | listeners.splice(index, 1); | |
129 | if(!listeners.length){ | |
130 | // no more listeners, remove the query updater too | |
131 | queryUpdaters.splice(array.indexOf(queryUpdaters, queryUpdater), 1); | |
132 | } | |
133 | } | |
134 | } | |
135 | }; | |
136 | }; | |
137 | } | |
138 | return results; | |
139 | }; | |
140 | var inMethod; | |
141 | function whenFinished(method, action){ | |
142 | var original = store[method]; | |
143 | if(original){ | |
144 | store[method] = function(value){ | |
145 | if(inMethod){ | |
146 | // if one method calls another (like add() calling put()) we don't want two events | |
147 | return original.apply(this, arguments); | |
148 | } | |
149 | inMethod = true; | |
150 | try{ | |
151 | var results = original.apply(this, arguments); | |
152 | Deferred.when(results, function(results){ | |
153 | action((typeof results == "object" && results) || value); | |
154 | }); | |
155 | return results; | |
156 | }finally{ | |
157 | inMethod = false; | |
158 | } | |
159 | }; | |
160 | } | |
161 | } | |
162 | // monitor for updates by listening to these methods | |
163 | whenFinished("put", function(object){ | |
164 | store.notify(object, store.getIdentity(object)); | |
165 | }); | |
166 | whenFinished("add", function(object){ | |
167 | store.notify(object); | |
168 | }); | |
169 | whenFinished("remove", function(id){ | |
170 | store.notify(undefined, id); | |
171 | }); | |
172 | ||
173 | return store; | |
174 | }; | |
175 | }); |