UNPKG

5.38 kBJavaScriptView Raw
1var _ = require("underscore"),
2 util = require("./util"),
3 EventEmitter = require("events").EventEmitter,
4 handlers = require("./handlers");
5
6var Model =
7module.exports = util.subclass.call(EventEmitter, {
8
9 constructor: function(value) {
10 EventEmitter.call(this);
11 this.setMaxListeners(0);
12
13 this._handlers = [];
14 this.children = {};
15
16 this.set([], value);
17 },
18
19 // returns the correct handler based on a value
20 _handler: function(val) {
21 var handler;
22
23 // first look through local
24 handler = _.find(this._handlers, function(h) {
25 return h.match(val);
26 });
27
28 // then try up the tree
29 if (handler == null && this.parent != null) {
30 handler = this.parent._handler(val);
31 }
32
33 // lastly look through global defaults
34 if (handler == null) {
35 handler = _.find(Model._defaultHandlers, function(h) {
36 return h.match(val);
37 });
38 }
39
40 return handler != null ? handler : handlers.default;
41 },
42
43 // adds a handler to use on any future model values
44 // secondary usage is to execute a handler method with arguments
45 handle: function(handler) {
46 if (_.isObject(handler)) {
47 handler = _.extend({}, defaultHandler, handler);
48 this._handlers.unshift(handler);
49 return this;
50 }
51
52 else if (_.isString(handler)) {
53 var handle = this.__handle__;
54
55 // create if doesn't exist
56 if (handler == "construct" || !_.isFunction(handle) || handle.value !== this.value) {
57 handle = Model.createHandle(this, this.value);
58 handle.value = this.value;
59 this.__handle__ = handle;
60 }
61
62 return handle.apply(null, _.toArray(arguments));
63 }
64 },
65
66 // creates a child model from a value at local path
67 _spawn: function(path) {
68 if (!_.isString(path)) throw new Error("Expecting path to be a string.");
69
70 var child, parent, val;
71 parent = this;
72 val = this.handle("get", path);
73 child = new (this.constructor)(val);
74
75 child.parent = parent;
76 child.on("change", onChange);
77
78 return child;
79
80 function onChange(summary, options) {
81 if (options.bubble === false) return;
82
83 if (!summary.keypath.length) {
84 // reset value to generic object if parent is a leaf node
85 if (parent.handle("isLeaf")) {
86 if (!options.remove) {
87 var reset = {};
88 reset[path] = summary.value;
89 parent.set([], reset, _.defaults({ reset: true }, options));
90 }
91
92 return;
93 }
94
95 // otherwise do a local set at the path
96 else {
97 if (options.remove) parent.handle("deleteProperty", path);
98 else parent.handle("set", path, summary.value);
99 }
100 }
101
102 parent.emit("change", _.defaults({
103 keypath: [ path ].concat(summary.keypath)
104 }, summary), options);
105 }
106 },
107
108 // returns the model at path, deeply
109 getModel: function(parts) {
110 parts = util.splitPath(parts);
111 if (!parts.length) return this;
112
113 var path = parts[0],
114 rest = parts.slice(1),
115 model;
116
117 if (this.children[path] != null) model = this.children[path];
118 else model = this.children[path] = this._spawn(path);
119
120 return model.getModel(rest);
121 },
122
123 // return the value of the model at path, deeply
124 get: function(path) {
125 return this.getModel(path).value;
126 },
127
128 // the own properties of the model's value
129 keys: function() { return this.handle("keys"); },
130
131 // sets a value at path, deeply
132 set: function(parts, value, options) {
133 // accept .set(value)
134 if (value == null && parts != null && !_.isArray(parts) && !_.isString(parts)) {
135 value = parts;
136 parts = [];
137 }
138
139 parts = util.splitPath(parts);
140 options = options || {};
141
142 // no path is a merge or reset
143 if (!parts.length) {
144
145 // try merge or reset
146 if (options.reset || this.handle("isLeaf") || this.handle("merge", value) === false) {
147
148 var oval = this.value;
149 this.handle("destroy");
150 this.value = options.remove ? void 0 : value;
151 this.handle("construct");
152
153 if (options.notify !== false && (oval !== this.value || options.remove)) {
154 this.notify([], this.value, oval, options);
155 }
156
157 }
158 }
159
160 // otherwise recurse to the correct model and try again
161 else {
162 this.getModel(parts).set([], value, options);
163 }
164
165 return this;
166 },
167
168 // removes the value at path
169 unset: function(path, options) {
170 return this.set(path || [], true, _.extend({ remove: true }, options));
171 },
172
173 // let's the model and its children know that something changed
174 notify: function(path, nval, oval, options) {
175 var silent, summary, child, childOptions, nval;
176
177 // notify only works on the model at path
178 if (!_.isArray(path) || path.length) {
179 return this.getModel(path).notify([], nval, oval, options);
180 }
181
182 options = options || {};
183 childOptions = _.extend({ reset: true }, options, { bubble: false });
184 summary = {
185 model: this,
186 type: util.changeType(nval, oval),
187 keypath: [],
188 value: nval,
189 oldValue: oval
190 };
191
192 // reset all the children values
193 _.each(this.children, function(c, p) {
194 c.set([], this.handle("get", p), childOptions);
195 }, this);
196
197 // announce the change
198 this.emit("change", summary, options);
199
200 return summary;
201 },
202
203}, {
204
205 extend: util.subclass,
206 _defaultHandlers: require("./handlers"),
207
208 isModel: function(obj) {
209 return obj instanceof Model;
210 },
211
212 // creates a focused handle function from model and value
213 createHandle: function(model, val) {
214 var handler = model._handler(val);
215
216 return function(m) {
217 var args = _.toArray(arguments).slice(1);
218 args.unshift(val);
219 return handler[m].apply(model, args);
220 }
221 }
222
223});
\No newline at end of file