]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dojo/store/Observable", ["../_base/kernel", "../_base/lang", "../_base/Deferred", "../_base/array" /*=====, "./api/Store" =====*/ |
2 | ], function(kernel, lang, Deferred, array /*=====, Store =====*/){ | |
3 | ||
4 | // module: | |
5 | // dojo/store/Observable | |
6 | ||
7 | var Observable = function(/*Store*/ store){ | |
8 | // summary: | |
9 | // The Observable store wrapper takes a store and sets an observe method on query() | |
10 | // results that can be used to monitor results for changes. | |
11 | // | |
12 | // description: | |
13 | // Observable wraps an existing store so that notifications can be made when a query | |
14 | // is performed. | |
15 | // | |
16 | // example: | |
17 | // Create a Memory store that returns an observable query, and then log some | |
18 | // information about that query. | |
19 | // | |
20 | // | var store = Observable(new Memory({ | |
21 | // | data: [ | |
22 | // | {id: 1, name: "one", prime: false}, | |
23 | // | {id: 2, name: "two", even: true, prime: true}, | |
24 | // | {id: 3, name: "three", prime: true}, | |
25 | // | {id: 4, name: "four", even: true, prime: false}, | |
26 | // | {id: 5, name: "five", prime: true} | |
27 | // | ] | |
28 | // | })); | |
29 | // | var changes = [], results = store.query({ prime: true }); | |
30 | // | var observer = results.observe(function(object, previousIndex, newIndex){ | |
31 | // | changes.push({previousIndex:previousIndex, newIndex:newIndex, object:object}); | |
32 | // | }); | |
33 | // | |
34 | // See the Observable tests for more information. | |
35 | ||
36 | var undef, queryUpdaters = [], revision = 0; | |
37 | // a Comet driven store could directly call notify to notify observers when data has | |
38 | // changed on the backend | |
39 | // create a new instance | |
40 | store = lang.delegate(store); | |
41 | ||
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){ | |
109 | // we don't have a queryEngine, so we can't provide any information | |
110 | // about where it was inserted or moved to. If it is an update, we leave it's position alone, other we at least indicate a new object | |
111 | if(existingId !== undef){ | |
112 | // an update, keep the index the same | |
113 | insertedInto = removedFrom; | |
114 | }else if(!options.start){ | |
115 | // a new object | |
116 | insertedInto = store.defaultIndex || 0; | |
117 | resultsArray.splice(insertedInto, 0, changed); | |
118 | } | |
119 | } | |
120 | if((removedFrom > -1 || insertedInto > -1) && | |
121 | (includeObjectUpdates || !queryExecutor || (removedFrom != insertedInto))){ | |
122 | var copyListeners = listeners.slice(); | |
123 | for(i = 0;listener = copyListeners[i]; i++){ | |
124 | listener(changed || removedObject, removedFrom, insertedInto); | |
125 | } | |
126 | } | |
127 | }); | |
128 | }); | |
129 | } | |
130 | var handle = {}; | |
131 | // TODO: Remove cancel in 2.0. | |
132 | handle.remove = handle.cancel = function(){ | |
133 | // remove this listener | |
134 | var index = array.indexOf(listeners, listener); | |
135 | if(index > -1){ // check to make sure we haven't already called cancel | |
136 | listeners.splice(index, 1); | |
137 | if(!listeners.length){ | |
138 | // no more listeners, remove the query updater too | |
139 | queryUpdaters.splice(array.indexOf(queryUpdaters, queryUpdater), 1); | |
140 | } | |
141 | } | |
142 | }; | |
143 | return handle; | |
144 | }; | |
145 | } | |
146 | return results; | |
147 | }; | |
148 | var inMethod; | |
149 | function whenFinished(method, action){ | |
150 | var original = store[method]; | |
151 | if(original){ | |
152 | store[method] = function(value){ | |
153 | if(inMethod){ | |
154 | // if one method calls another (like add() calling put()) we don't want two events | |
155 | return original.apply(this, arguments); | |
156 | } | |
157 | inMethod = true; | |
158 | try{ | |
159 | var results = original.apply(this, arguments); | |
160 | Deferred.when(results, function(results){ | |
161 | action((typeof results == "object" && results) || value); | |
162 | }); | |
163 | return results; | |
164 | }finally{ | |
165 | inMethod = false; | |
166 | } | |
167 | }; | |
168 | } | |
169 | } | |
170 | // monitor for updates by listening to these methods | |
171 | whenFinished("put", function(object){ | |
172 | store.notify(object, store.getIdentity(object)); | |
173 | }); | |
174 | whenFinished("add", function(object){ | |
175 | store.notify(object); | |
176 | }); | |
177 | whenFinished("remove", function(id){ | |
178 | store.notify(undefined, id); | |
179 | }); | |
180 | ||
181 | return store; | |
182 | }; | |
183 | ||
184 | lang.setObject("dojo.store.Observable", Observable); | |
185 | ||
186 | return Observable; | |
187 | }); |