UNPKG

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