Observable.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojo.store.Observable"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojo.store.Observable"] = true;
  8. dojo.provide("dojo.store.Observable");
  9. dojo.getObject("store", true, dojo);
  10. dojo.store.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. var queryUpdaters = [], revision = 0;
  39. // a Comet driven store could directly call notify to notify observers when data has
  40. // changed on the backend
  41. store.notify = function(object, existingId){
  42. revision++;
  43. var updaters = queryUpdaters.slice();
  44. for(var i = 0, l = updaters.length; i < l; i++){
  45. updaters[i](object, existingId);
  46. }
  47. };
  48. var originalQuery = store.query;
  49. store.query = function(query, options){
  50. options = options || {};
  51. var results = originalQuery.apply(this, arguments);
  52. if(results && results.forEach){
  53. var nonPagedOptions = dojo.mixin({}, options);
  54. delete nonPagedOptions.start;
  55. delete nonPagedOptions.count;
  56. var queryExecutor = store.queryEngine && store.queryEngine(query, nonPagedOptions);
  57. var queryRevision = revision;
  58. var listeners = [], queryUpdater;
  59. results.observe = function(listener, includeObjectUpdates){
  60. if(listeners.push(listener) == 1){
  61. // first listener was added, create the query checker and updater
  62. queryUpdaters.push(queryUpdater = function(changed, existingId){
  63. dojo.when(results, function(resultsArray){
  64. var atEnd = resultsArray.length != options.count;
  65. var i;
  66. if(++queryRevision != revision){
  67. throw new Error("Query is out of date, you must observe() the query prior to any data modifications");
  68. }
  69. var removedObject, removedFrom = -1, insertedInto = -1;
  70. if(existingId){
  71. // remove the old one
  72. for(i = 0, l = resultsArray.length; i < l; i++){
  73. var object = resultsArray[i];
  74. if(store.getIdentity(object) == existingId){
  75. removedObject = object;
  76. removedFrom = i;
  77. 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
  78. resultsArray.splice(i, 1);
  79. }
  80. break;
  81. }
  82. }
  83. }
  84. if(queryExecutor){
  85. // add the new one
  86. if(changed &&
  87. // if a matches function exists, use that (probably more efficient)
  88. (queryExecutor.matches ? queryExecutor.matches(changed) : queryExecutor([changed]).length)){
  89. if(removedFrom > -1){
  90. // put back in the original slot so it doesn't move unless it needs to (relying on a stable sort below)
  91. resultsArray.splice(removedFrom, 0, changed);
  92. }else{
  93. resultsArray.push(changed);
  94. }
  95. insertedInto = dojo.indexOf(queryExecutor(resultsArray), changed);
  96. if((options.start && insertedInto == 0) ||
  97. (!atEnd && insertedInto == resultsArray.length -1)){
  98. // if it is at the end of the page, assume it goes into the prev or next page
  99. insertedInto = -1;
  100. }
  101. }
  102. }else if(changed){
  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. listeners.splice(dojo.indexOf(listeners, listener), 1);
  121. if(!listeners.length){
  122. // no more listeners, remove the query updater too
  123. queryUpdaters.splice(dojo.indexOf(queryUpdaters, queryUpdater), 1);
  124. }
  125. }
  126. };
  127. };
  128. }
  129. return results;
  130. };
  131. var inMethod;
  132. function whenFinished(method, action){
  133. var original = store[method];
  134. if(original){
  135. store[method] = function(value){
  136. if(inMethod){
  137. // if one method calls another (like add() calling put()) we don't want two events
  138. return original.apply(this, arguments);
  139. }
  140. inMethod = true;
  141. try{
  142. return dojo.when(original.apply(this, arguments), function(results){
  143. action((typeof results == "object" && results) || value);
  144. return results;
  145. });
  146. }finally{
  147. inMethod = false;
  148. }
  149. };
  150. }
  151. }
  152. // monitor for updates by listening to these methods
  153. whenFinished("put", function(object){
  154. store.notify(object, store.getIdentity(object));
  155. });
  156. whenFinished("add", function(object){
  157. store.notify(object);
  158. });
  159. whenFinished("remove", function(id){
  160. store.notify(undefined, id);
  161. });
  162. return store;
  163. };
  164. }