Observable.js 6.4 KB

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