UNPKG

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