UNPKG

7.9 kBJavaScriptView Raw
1var _ = require("underscore");
2
3// tests value as pojo (plain old javascript object)
4var isPlainObject =
5exports.isPlainObject = function(obj) {
6 return obj != null && obj.__proto__ === Object.prototype;
7}
8
9// the subclassing function found in Backbone
10var subclass =
11exports.subclass = function(protoProps, staticProps) {
12 var parent = this;
13 var child;
14
15 // The constructor function for the new subclass is either defined by you
16 // (the "constructor" property in your `extend` definition), or defaulted
17 // by us to simply call the parent's constructor.
18 if (protoProps && _.has(protoProps, 'constructor')) {
19 child = protoProps.constructor;
20 } else {
21 child = function(){ return parent.apply(this, arguments); };
22 }
23
24 // Add static properties to the constructor function, if supplied.
25 _.extend(child, parent, staticProps);
26
27 // Set the prototype chain to inherit from `parent`, without calling
28 // `parent`'s constructor function.
29 var Surrogate = function(){ this.constructor = child; };
30 Surrogate.prototype = parent.prototype;
31 child.prototype = new Surrogate;
32
33 // Add prototype properties (instance properties) to the subclass,
34 // if supplied.
35 if (protoProps) _.extend(child.prototype, protoProps);
36
37 // Set a convenience property in case the parent's prototype is needed
38 // later.
39 child.__super__ = parent.prototype;
40
41 return child;
42}
43
44// cleans an array of path parts
45var sanitizePathParts =
46exports.sanitizePathParts = function(parts) {
47 return parts.filter(function(a) {
48 return a != null && a !== "";
49 }).map(function(a) {
50 var s = a.toString();
51 if (s[0] === ".") s = s.substr(1);
52 if (s.substr(-1) === ".") s = s.substr(0, s.length - 1);
53 return s;
54 });
55}
56
57// splits a path by period
58var splitPath =
59exports.splitPath = function(path) {
60 var parts = _.isArray(path) ? path : _.isString(path) ? path.split(".") : [ path ];
61 if (parts.length > 1 && parts[0] === "") parts[0] = "this";
62 return sanitizePathParts(parts);
63}
64
65// parses a string path as a dynamic path
66var parsePath =
67exports.parsePath = function(path) {
68 return splitPath(path).map(function(part) {
69 if (part.indexOf("*") > -1 && part !== "**") {
70 return new RegExp("^" + part.split("*").join("([^\\.]*)") + "$");
71 }
72
73 return part;
74 });
75}
76
77// concats path parts together into a string
78var joinPathParts =
79exports.joinPathParts = function() {
80 return sanitizePathParts(_.flatten(_.toArray(arguments))).join(".");
81}
82
83// deeply looks for a value at path in obj
84var get =
85exports.get = function(obj, parts, getter) {
86 parts = splitPath(parts);
87
88 // custom getter
89 if (!_.isFunction(getter)) {
90 getter = function(obj, path) { return obj[path]; }
91 }
92
93 while (parts.length) {
94 if (obj == null) return;
95 obj = getter(obj, parts.shift());
96 }
97
98 return obj;
99}
100
101// reduces paths so they are unique and short
102var findShallowestUniquePaths =
103exports.findShallowestUniquePaths = function(paths) {
104 return paths.reduce(function(m, keys) {
105 // first check if a shorter or equal path exists
106 if (m.some(function(k) {
107 return arrayStartsWith(keys, k);
108 })) return m;
109
110 // next check for any longer paths that need to be removed
111 m.slice(0).forEach(function(k, index) {
112 if (arrayStartsWith(k, keys)) m.splice(index, 1);
113 });
114
115 // and lastly add the path to output
116 m.push(keys);
117 return m;
118 }, []);
119}
120
121// determines if the values of array match the start of another array
122// can be read as: does [a1] start with [a2]
123var arrayStartsWith =
124exports.arrayStartsWith = function(a1, a2) {
125 var max = a2.length;
126 return max <= a1.length && _.isEqual(a2, a1.slice(0, max));
127}
128
129// array write operations
130var mutatorMethods = [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ];
131
132// patches an array so we can listen to write operations
133var patchArray =
134exports.patchArray = function(arr) {
135 if (arr._patched) return arr;
136
137 var patchedArrayProto = [],
138 observers = [];
139
140 Object.defineProperty(patchedArrayProto, "_patched", { value: true });
141 Object.defineProperty(patchedArrayProto, "_observers", { value: [] });
142
143 Object.defineProperty(patchedArrayProto, "observe", {
144 value: function(fn) {
145 if (typeof fn !== "function") throw new Error("Expecting function to observe with.");
146 this._observers.push(fn);
147 return this;
148 }
149 });
150
151 Object.defineProperty(patchedArrayProto, "stopObserving", {
152 value: function(fn) {
153 var index = this._observers.indexOf(fn);
154 if (index > -1) this._observers.splice(index, 1);
155 return this;
156 }
157 });
158
159 mutatorMethods.forEach(function(methodName) {
160 Object.defineProperty(patchedArrayProto, methodName, {
161 value: method
162 });
163
164 function method() {
165 var spliceEquivalent, summary, start,
166 original, size, i, index, result;
167
168 // push, pop, shift and unshift can all be represented as a splice operation.
169 // this makes life easier later
170 spliceEquivalent = getSpliceEquivalent(this, methodName, _.toArray(arguments));
171 summary = summariseSpliceOperation(this, spliceEquivalent);
172
173 // make a copy of the original values
174 if (summary != null) {
175 start = summary.start;
176 original = Array.prototype.slice.call(this, start, !summary.balance ? start + summary.added : void 0);
177 size = (summary.balance > 0 ? summary.added : 0) + original.length;
178 } else {
179 start = 0;
180 original = Array.prototype.slice.call(this, 0);
181 size = original.length;
182 }
183
184 // apply the underlying method
185 result = Array.prototype[methodName].apply(this, arguments);
186
187 // trigger changes
188 for (i = 0; i < size; i++) {
189 index = i + start;
190 this._observers.forEach(function(fn) {
191 fn.call(this, index, this[index], original[i]);
192 }, this);
193 }
194
195 return result;
196 };
197 });
198
199 if (({}).__proto__) arr.__proto__ = patchedArrayProto;
200 else {
201 mutatorMethods.forEach(function(methodName) {
202 Object.defineProperty(arr, methodName, {
203 value: patchedArrayProto[methodName],
204 configurable: true
205 });
206 });
207 }
208
209 return arr;
210}
211
212// converts array write operations into splice equivalent arguments
213var getSpliceEquivalent =
214exports.getSpliceEquivalent = function ( array, methodName, args ) {
215 switch ( methodName ) {
216 case 'splice':
217 return args;
218
219 case 'sort':
220 case 'reverse':
221 return null;
222
223 case 'pop':
224 if ( array.length ) {
225 return [ -1 ];
226 }
227 return null;
228
229 case 'push':
230 return [ array.length, 0 ].concat( args );
231
232 case 'shift':
233 return [ 0, 1 ];
234
235 case 'unshift':
236 return [ 0, 0 ].concat( args );
237 }
238}
239
240// returns a summary pf how an array will be changed after the splice operation
241var summariseSpliceOperation =
242exports.summariseSpliceOperation = function ( array, args ) {
243 var start, addedItems, removedItems, balance;
244
245 if ( !args ) {
246 return null;
247 }
248
249 // figure out where the changes started...
250 start = +( args[0] < 0 ? array.length + args[0] : args[0] );
251
252 // ...and how many items were added to or removed from the array
253 addedItems = Math.max( 0, args.length - 2 );
254 removedItems = ( args[1] !== undefined ? args[1] : array.length - start );
255
256 // It's possible to do e.g. [ 1, 2, 3 ].splice( 2, 2 ) - i.e. the second argument
257 // means removing more items from the end of the array than there are. In these
258 // cases we need to curb JavaScript's enthusiasm or we'll get out of sync
259 removedItems = Math.min( removedItems, array.length - start );
260
261 balance = addedItems - removedItems;
262
263 return {
264 start: start,
265 balance: balance,
266 added: addedItems,
267 removed: removedItems
268 };
269}
270
271// tests a node against a selector
272exports.matchSelector = function(node, selector) {
273 var nodes, i;
274
275 nodes = ( node.parentNode || node.ownerDocument ).querySelectorAll( selector );
276
277 i = nodes.length;
278 while ( i-- ) {
279 if ( nodes[i] === node ) {
280 return true;
281 }
282 }
283
284 return false;
285}
286
287// returns the type of changes based on old and new values
288// expects oval !== nval
289exports.changeType = function(nval, oval) {
290 return _.isUndefined(oval) ? "add" : _.isUndefined(nval) ? "delete" : "update";
291}
\No newline at end of file