UNPKG

7.97 kBJavaScriptView Raw
1var _ = require("underscore"),
2 util = require("./util"),
3 EventEmitter = require("events").EventEmitter,
4 Model = require("./model"),
5 Deps = require("./deps");
6
7var proto = {
8
9 constructor: function(model) {
10 EventEmitter.call(this);
11 this.setMaxListeners(0);
12
13 // convert data to model if isn't one already
14 if (!Model.isModel(model)) {
15 var data = model;
16 model = new Model(_.result(this, "defaults"));
17 if (!_.isUndefined(data)) model.set([], data);
18 }
19
20 this.models = [ model ];
21 this._observers = [];
22 this._deps = [];
23 this.initialize();
24 },
25
26 initialize: function() {},
27
28 // adds a model to the set
29 addModel: function(model) {
30 // accept scopes and arrays, but reduce them to models
31 if (Scope.isScope(model)) this.addModel(model.models);
32 else if (_.isArray(model)) {
33 model.forEach(function(m) { this.addModel(m); }, this);
34 }
35
36 else {
37 if (!Model.isModel(model)) throw new Error("Expecting model.");
38 if (!~this.models.indexOf(model)) {
39 this.models.push(model);
40
41 // add observers
42 this._observers.forEach(function(ob) {
43 model.on("change", ob.onChange);
44 });
45 }
46 }
47
48 return this;
49 },
50
51 // removes a previously added model
52 removeModel: function(model) {
53 if (Scope.isScope(model)) this.removeModel(model.models);
54 else if (_.isArray(model)) {
55 model.forEach(function(m) { this.removeModel(m); }, this);
56 }
57
58 else {
59 var index = this.models.indexOf(model);
60 if (~index) {
61 this.models.splice(index, 1);
62
63 // strip observers
64 this._observers.forEach(function(ob) {
65 model.removeListener("change", ob.onChange);
66 });
67 }
68 }
69
70 return this;
71 },
72
73 // returns the first model whose value at path isn't null
74 findModel: function(path) {
75 return _.find(this.models, function(model) {
76 return !_.isUndefined(model.get(path));
77 });
78 },
79
80 get: function(parts) {
81 var val, model;
82
83 parts = util.splitPath(parts);
84
85 if (parts[0] === "this") {
86 parts.shift();
87 val = this.models[0].get(parts);
88 }
89
90 else {
91 model = this.findModel(parts);
92 if (model != null) val = model.get(parts);
93 }
94
95 // execute functions
96 if (_.isFunction(val)) val = val.call(this);
97
98 // always depend
99 if (Deps.active) this.depend(parts);
100
101 return val;
102 },
103
104 // registers a dependency at path and observes changes
105 depend: function(parts) {
106 var path = util.joinPathParts(parts),
107 dep = this._deps[path];
108
109 // create if doesn't exist
110 if (dep == null) {
111 dep = this._deps[path] = new Deps.Dependency;
112 dep._observer = this.observe(parts, function() { dep.changed(); });
113 }
114
115 dep.depend();
116 return this;
117 },
118
119 // reruns fn anytime dependencies change
120 autorun: function(fn) {
121 return Deps.autorun(fn.bind(this));
122 },
123
124 // calls fn when path changes
125 observe: function(path, fn) {
126 if (!_.isFunction(fn)) throw new Error("Expecting a function to call on change.");
127
128 var matchParts = _.isArray(path) ? path : util.parsePath(path),
129 self = this;
130
131 // remember the observer so we can kill it later
132 this._observers.push({
133 path: path,
134 fn: fn,
135 onChange: onChange
136 });
137
138 // apply to all existing models
139 this.models.forEach(function(m) {
140 m.on("change", onChange);
141 });
142
143 return this;
144
145 function onChange(chg) {
146 var keys, newval, oldval, model,
147 ngetter, ogetter, parts, part, base, paths, i,
148 cmodel, cindex, pmodel, omodel;
149
150 // clone parts so we don't affect the original
151 parts = matchParts.slice(0);
152 keys = chg.keypath;
153 newval = chg.value;
154 oldval = chg.oldValue;
155 model = chg.model;
156 pmodel = model;
157
158 // we need to get the true old and new values based on all the models
159 if (chg.type !== "update") {
160 cmodel = self.findModel(chg.keypath);
161
162 if (cmodel != null) {
163 cindex = self.models.indexOf(cmodel);
164
165 if (cmodel === this) {
166 omodel = _.find(self.models.slice(cindex + 1), function(model) {
167 return !_.isUndefined(model.get(path));
168 });
169
170 if (omodel != null) {
171 pmodel = omodel.getModel(keys);
172 oldval = pmodel.value;
173 }
174
175 } else if (cindex > self.models.indexOf(this)) {
176 pmodel = model;
177 model = cmodel.getModel(keys);
178 newval = model.value;
179
180 } else return;
181 }
182 }
183
184 // traverse through cparts
185 // a mismatch means we don't need to be here
186 for (i = 0; i < keys.length; i++) {
187 part = parts.shift();
188 if (_.isRegExp(part) && part.test(keys[i])) continue;
189 if (part === "**") {
190 // look ahead
191 if (parts[0] == null || parts[0] !== keys[i + 1]) {
192 parts.unshift(part);
193 }
194 continue;
195 }
196 if (part !== keys[i]) return;
197 }
198
199 paths = [];
200 base = util.joinPathParts(keys);
201
202 // generate a list of effected paths
203 findAllMatchingPaths.call(this, model, newval, parts, paths);
204 findAllMatchingPaths.call(this, pmodel, oldval, parts, paths);
205 paths = util.findShallowestUniquePaths(paths);
206
207 // getters for retrieving values at path
208 ngetter = function(obj, path) {
209 return Model.createHandle(model, obj)("get", path);
210 }
211
212 if (model === pmodel) ogetter = ngetter;
213 else ogetter = function(obj, path) {
214 return Model.createHandle(pmodel, obj)("get", path);
215 }
216
217 // fire the callback on each path that changed
218 paths.forEach(function(keys, index, list) {
219 var path, localModel, nval, oval;
220
221 nval = util.get(newval, keys, ngetter),
222 oval = util.get(oldval, keys, ogetter);
223 if (nval === oval) return;
224
225 fn.call(self, {
226 model: model.getModel(keys),
227 previousModel: pmodel.getModel(keys),
228 path: util.joinPathParts(base, keys),
229 type: util.changeType(nval, oval),
230 value: nval,
231 oldValue: oval
232 });
233 });
234 }
235 },
236
237 stopObserving: function(path, fn) {
238 var obs;
239
240 if (_.isFunction(path) && fn == null) {
241 fn = path;
242 path = null;
243 }
244
245 if (path == null && fn == null) {
246 obs = this._observers;
247 this._observers = [];
248 }
249
250 else {
251 obs = this._observers.filter(function(o) {
252 return (path == null || path === o.path) && (fn == null || fn === o.fn);
253 });
254 }
255
256 obs.forEach(function(o) {
257 this.models.forEach(function(m) {
258 m.removeListener("change", o.onChange);
259 });
260
261 var index = this._observers.indexOf(o);
262 if (~index) this._observers.splice(index, 1);
263 }, this);
264
265 return this;
266 }
267
268};
269
270// chainable proxy methods
271[ "handle", "set", "unset", "setHidden" ]
272.forEach(function(method) {
273 proto[method] = function() {
274 var model = this.models[0];
275 model[method].apply(model, arguments);
276 return this;
277 }
278});
279
280// proxy methods which don't return this
281[ "getModel", "keys", "notify" ]
282.forEach(function(method) {
283 proto[method] = function() {
284 var model = this.models[0];
285 return model[method].apply(model, arguments);
286 }
287});
288
289var Scope =
290module.exports = util.subclass.call(EventEmitter, proto, {
291
292 extend: util.subclass,
293
294 isScope: function(obj) {
295 return obj instanceof Scope;
296 }
297
298});
299
300// deeply traverses a value in search of all paths that match parts
301function findAllMatchingPaths(model, value, parts, paths, base) {
302 if (paths == null) paths = [];
303 if (base == null) base = [];
304
305 if (!parts.length) {
306 paths.push(base);
307 return paths;
308 }
309
310 var handle = Model.createHandle(model, value),
311 part = parts[0],
312 rest = parts.slice(1);
313
314 if (_.isRegExp(part)) {
315 handle("keys").forEach(function(k) {
316 findAllMatchingPaths.call(this, model.getModel(k), handle("get", k), rest, paths, base.concat(k));
317 }, this);
318 } else if (part === "**") {
319 if (handle("isLeaf")) {
320 if (!rest.length) paths.push(base);
321 return paths;
322 }
323
324 handle("keys").forEach(function(k) {
325 var _rest = rest,
326 _base = base;
327
328 // look ahead
329 if (rest[0] == null || rest[0] !== k) {
330 _rest = [part].concat(rest);
331 _base = base.concat(k);
332 }
333
334 findAllMatchingPaths.call(this, model.getModel(k), handle("get", k), _rest, paths, _base);
335 }, this);
336 } else {
337 findAllMatchingPaths.call(this, model.getModel(part), handle("get", part), rest, paths, base.concat(part));
338 }
339
340 return paths;
341}
\No newline at end of file