UNPKG

22 kBJavaScriptView Raw
1(function() {
2 "use strict";
3
4 // https://github.com/facebook/react/blob/v15.0.1/src/isomorphic/classic/element/ReactElement.js#L21
5 var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element');
6 var REACT_ELEMENT_TYPE_FALLBACK = 0xeac7;
7
8 function addPropertyTo(target, methodName, value) {
9 Object.defineProperty(target, methodName, {
10 enumerable: false,
11 configurable: false,
12 writable: false,
13 value: value
14 });
15 }
16
17 function banProperty(target, methodName) {
18 addPropertyTo(target, methodName, function() {
19 throw new ImmutableError("The " + methodName +
20 " method cannot be invoked on an Immutable data structure.");
21 });
22 }
23
24 var immutabilityTag = "__immutable_invariants_hold";
25
26 function addImmutabilityTag(target) {
27 addPropertyTo(target, immutabilityTag, true);
28 }
29
30 function isImmutable(target) {
31 if (typeof target === "object") {
32 return target === null || Boolean(
33 Object.getOwnPropertyDescriptor(target, immutabilityTag)
34 );
35 } else {
36 // In JavaScript, only objects are even potentially mutable.
37 // strings, numbers, null, and undefined are all naturally immutable.
38 return true;
39 }
40 }
41
42 function isEqual(a, b) {
43 // Avoid false positives due to (NaN !== NaN) evaluating to true
44 return (a === b || (a !== a && b !== b));
45 }
46
47 function isMergableObject(target) {
48 return target !== null && typeof target === "object" && !(Array.isArray(target)) && !(target instanceof Date);
49 }
50
51 var mutatingObjectMethods = [
52 "setPrototypeOf"
53 ];
54
55 var nonMutatingObjectMethods = [
56 "keys"
57 ];
58
59 var mutatingArrayMethods = mutatingObjectMethods.concat([
60 "push", "pop", "sort", "splice", "shift", "unshift", "reverse"
61 ]);
62
63 var nonMutatingArrayMethods = nonMutatingObjectMethods.concat([
64 "map", "filter", "slice", "concat", "reduce", "reduceRight"
65 ]);
66
67 var mutatingDateMethods = mutatingObjectMethods.concat([
68 "setDate", "setFullYear", "setHours", "setMilliseconds", "setMinutes", "setMonth", "setSeconds",
69 "setTime", "setUTCDate", "setUTCFullYear", "setUTCHours", "setUTCMilliseconds", "setUTCMinutes",
70 "setUTCMonth", "setUTCSeconds", "setYear"
71 ]);
72
73 function ImmutableError(message) {
74 var err = new Error(message);
75 // TODO: Consider `Object.setPrototypeOf(err, ImmutableError);`
76 err.__proto__ = ImmutableError;
77
78 return err;
79 }
80 ImmutableError.prototype = Error.prototype;
81
82 function makeImmutable(obj, bannedMethods) {
83 // Tag it so we can quickly tell it's immutable later.
84 addImmutabilityTag(obj);
85
86 if ("development" !== "production") {
87 // Make all mutating methods throw exceptions.
88 for (var index in bannedMethods) {
89 if (bannedMethods.hasOwnProperty(index)) {
90 banProperty(obj, bannedMethods[index]);
91 }
92 }
93
94 // Freeze it and return it.
95 Object.freeze(obj);
96 }
97
98 return obj;
99 }
100
101 function makeMethodReturnImmutable(obj, methodName) {
102 var currentMethod = obj[methodName];
103
104 addPropertyTo(obj, methodName, function() {
105 return Immutable(currentMethod.apply(obj, arguments));
106 });
107 }
108
109 function arraySet(idx, value, config) {
110 var deep = config && config.deep;
111
112 if (idx in this) {
113 if (deep && this[idx] !== value && isMergableObject(value) && isMergableObject(this[idx])) {
114 value = this[idx].merge(value, {deep: true, mode: 'replace'});
115 }
116 if (isEqual(this[idx], value)) {
117 return this;
118 }
119 }
120
121 var mutable = asMutableArray.call(this);
122 mutable[idx] = Immutable(value);
123 return makeImmutableArray(mutable);
124 }
125
126 var immutableEmptyArray = Immutable([]);
127
128 function arraySetIn(pth, value, config) {
129 var head = pth[0];
130
131 if (pth.length === 1) {
132 return arraySet.call(this, head, value, config);
133 } else {
134 var tail = pth.slice(1);
135 var thisHead = this[head];
136 var newValue;
137
138 if (typeof(thisHead) === "object" && thisHead !== null && typeof(thisHead.setIn) === "function") {
139 // Might (validly) be object or array
140 newValue = thisHead.setIn(tail, value);
141 } else {
142 var nextHead = tail[0];
143 // If the next path part is a number, then we are setting into an array, else an object.
144 if (nextHead !== '' && isFinite(nextHead)) {
145 newValue = arraySetIn.call(immutableEmptyArray, tail, value);
146 } else {
147 newValue = objectSetIn.call(immutableEmptyObject, tail, value);
148 }
149 }
150
151 if (head in this && thisHead === newValue) {
152 return this;
153 }
154
155 var mutable = asMutableArray.call(this);
156 mutable[head] = newValue;
157 return makeImmutableArray(mutable);
158 }
159 }
160
161 function makeImmutableArray(array) {
162 // Don't change their implementations, but wrap these functions to make sure
163 // they always return an immutable value.
164 for (var index in nonMutatingArrayMethods) {
165 if (nonMutatingArrayMethods.hasOwnProperty(index)) {
166 var methodName = nonMutatingArrayMethods[index];
167 makeMethodReturnImmutable(array, methodName);
168 }
169 }
170
171 addPropertyTo(array, "flatMap", flatMap);
172 addPropertyTo(array, "asObject", asObject);
173 addPropertyTo(array, "asMutable", asMutableArray);
174 addPropertyTo(array, "set", arraySet);
175 addPropertyTo(array, "setIn", arraySetIn);
176 addPropertyTo(array, "update", update);
177 addPropertyTo(array, "updateIn", updateIn);
178
179 for(var i = 0, length = array.length; i < length; i++) {
180 array[i] = Immutable(array[i]);
181 }
182
183 return makeImmutable(array, mutatingArrayMethods);
184 }
185
186 function makeImmutableDate(date) {
187 addPropertyTo(date, "asMutable", asMutableDate);
188
189 return makeImmutable(date, mutatingDateMethods);
190 }
191
192 function asMutableDate() {
193 return new Date(this.getTime());
194 }
195
196 /**
197 * Effectively performs a map() over the elements in the array, using the
198 * provided iterator, except that whenever the iterator returns an array, that
199 * array's elements are added to the final result instead of the array itself.
200 *
201 * @param {function} iterator - The iterator function that will be invoked on each element in the array. It will receive three arguments: the current value, the current index, and the current object.
202 */
203 function flatMap(iterator) {
204 // Calling .flatMap() with no arguments is a no-op. Don't bother cloning.
205 if (arguments.length === 0) {
206 return this;
207 }
208
209 var result = [],
210 length = this.length,
211 index;
212
213 for (index = 0; index < length; index++) {
214 var iteratorResult = iterator(this[index], index, this);
215
216 if (Array.isArray(iteratorResult)) {
217 // Concatenate Array results into the return value we're building up.
218 result.push.apply(result, iteratorResult);
219 } else {
220 // Handle non-Array results the same way map() does.
221 result.push(iteratorResult);
222 }
223 }
224
225 return makeImmutableArray(result);
226 }
227
228 /**
229 * Returns an Immutable copy of the object without the given keys included.
230 *
231 * @param {array} keysToRemove - A list of strings representing the keys to exclude in the return value. Instead of providing a single array, this method can also be called by passing multiple strings as separate arguments.
232 */
233 function without(remove) {
234 // Calling .without() with no arguments is a no-op. Don't bother cloning.
235 if (typeof remove === "undefined" && arguments.length === 0) {
236 return this;
237 }
238
239 if (typeof remove !== "function") {
240 // If we weren't given an array, use the arguments list.
241 var keysToRemoveArray = (Array.isArray(remove)) ?
242 remove.slice() : Array.prototype.slice.call(arguments);
243
244 // Convert numeric keys to strings since that's how they'll
245 // come from the enumeration of the object.
246 keysToRemoveArray.forEach(function(el, idx, arr) {
247 if(typeof(el) === "number") {
248 arr[idx] = el.toString();
249 }
250 });
251
252 remove = function(val, key) {
253 return keysToRemoveArray.indexOf(key) !== -1;
254 };
255 }
256
257 var result = this.instantiateEmptyObject();
258
259 for (var key in this) {
260 if (this.hasOwnProperty(key) && remove(this[key], key) === false) {
261 result[key] = this[key];
262 }
263 }
264
265 return makeImmutableObject(result,
266 {instantiateEmptyObject: this.instantiateEmptyObject});
267 }
268
269 function asMutableArray(opts) {
270 var result = [], i, length;
271
272 if(opts && opts.deep) {
273 for(i = 0, length = this.length; i < length; i++) {
274 result.push(asDeepMutable(this[i]));
275 }
276 } else {
277 for(i = 0, length = this.length; i < length; i++) {
278 result.push(this[i]);
279 }
280 }
281
282 return result;
283 }
284
285 /**
286 * Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function
287 * will return an array of two elements - the first representing a key, the other
288 * a value. Then returns an Immutable Object constructed of those keys and values.
289 *
290 * @param {function} iterator - A function which should return an array of two elements - the first representing the desired key, the other the desired value.
291 */
292 function asObject(iterator) {
293 // If no iterator was provided, assume the identity function
294 // (suggesting this array is already a list of key/value pairs.)
295 if (typeof iterator !== "function") {
296 iterator = function(value) { return value; };
297 }
298
299 var result = {},
300 length = this.length,
301 index;
302
303 for (index = 0; index < length; index++) {
304 var pair = iterator(this[index], index, this),
305 key = pair[0],
306 value = pair[1];
307
308 result[key] = value;
309 }
310
311 return makeImmutableObject(result);
312 }
313
314 function asDeepMutable(obj) {
315 if (
316 (!obj) ||
317 (typeof obj !== 'object') ||
318 (!Object.getOwnPropertyDescriptor(obj, immutabilityTag)) ||
319 (obj instanceof Date)
320 ) { return obj; }
321 return obj.asMutable({deep: true});
322 }
323
324 function quickCopy(src, dest) {
325 for (var key in src) {
326 if (Object.getOwnPropertyDescriptor(src, key)) {
327 dest[key] = src[key];
328 }
329 }
330
331 return dest;
332 }
333
334 /**
335 * Returns an Immutable Object containing the properties and values of both
336 * this object and the provided object, prioritizing the provided object's
337 * values whenever the same key is present in both objects.
338 *
339 * @param {object} other - The other object to merge. Multiple objects can be passed as an array. In such a case, the later an object appears in that list, the higher its priority.
340 * @param {object} config - Optional config object that contains settings. Supported settings are: {deep: true} for deep merge and {merger: mergerFunc} where mergerFunc is a function
341 * that takes a property from both objects. If anything is returned it overrides the normal merge behaviour.
342 */
343 function merge(other, config) {
344 // Calling .merge() with no arguments is a no-op. Don't bother cloning.
345 if (arguments.length === 0) {
346 return this;
347 }
348
349 if (other === null || (typeof other !== "object")) {
350 throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not " + JSON.stringify(other));
351 }
352
353 var receivedArray = (Array.isArray(other)),
354 deep = config && config.deep,
355 mode = config && config.mode || 'merge',
356 merger = config && config.merger,
357 result;
358
359 // Use the given key to extract a value from the given object, then place
360 // that value in the result object under the same key. If that resulted
361 // in a change from this object's value at that key, set anyChanges = true.
362 function addToResult(currentObj, otherObj, key) {
363 var immutableValue = Immutable(otherObj[key]);
364 var mergerResult = merger && merger(currentObj[key], immutableValue, config);
365 var currentValue = currentObj[key];
366
367 if ((result !== undefined) ||
368 (mergerResult !== undefined) ||
369 (!currentObj.hasOwnProperty(key)) ||
370 !isEqual(immutableValue, currentValue)) {
371
372 var newValue;
373
374 if (mergerResult) {
375 newValue = mergerResult;
376 } else if (deep && isMergableObject(currentValue) && isMergableObject(immutableValue)) {
377 newValue = currentValue.merge(immutableValue, config);
378 } else {
379 newValue = immutableValue;
380 }
381
382 if (!isEqual(currentValue, newValue) || !currentObj.hasOwnProperty(key)) {
383 if (result === undefined) {
384 // Make a shallow clone of the current object.
385 result = quickCopy(currentObj, currentObj.instantiateEmptyObject());
386 }
387
388 result[key] = newValue;
389 }
390 }
391 }
392
393 function clearDroppedKeys(currentObj, otherObj) {
394 for (var key in currentObj) {
395 if (!otherObj.hasOwnProperty(key)) {
396 if (result === undefined) {
397 // Make a shallow clone of the current object.
398 result = quickCopy(currentObj, currentObj.instantiateEmptyObject());
399 }
400 delete result[key];
401 }
402 }
403 }
404
405 var key;
406
407 // Achieve prioritization by overriding previous values that get in the way.
408 if (!receivedArray) {
409 // The most common use case: just merge one object into the existing one.
410 for (key in other) {
411 if (Object.getOwnPropertyDescriptor(other, key)) {
412 addToResult(this, other, key);
413 }
414 }
415 if (mode === 'replace') {
416 clearDroppedKeys(this, other);
417 }
418 } else {
419 // We also accept an Array
420 for (var index = 0, length = other.length; index < length; index++) {
421 var otherFromArray = other[index];
422
423 for (key in otherFromArray) {
424 if (otherFromArray.hasOwnProperty(key)) {
425 addToResult(result !== undefined ? result : this, otherFromArray, key);
426 }
427 }
428 }
429 }
430
431 if (result === undefined) {
432 return this;
433 } else {
434 return makeImmutableObject(result,
435 {instantiateEmptyObject: this.instantiateEmptyObject});
436 }
437 }
438
439 function objectReplace(value, config) {
440 var deep = config && config.deep;
441
442 // Calling .replace() with no arguments is a no-op. Don't bother cloning.
443 if (arguments.length === 0) {
444 return this;
445 }
446
447 if (value === null || typeof value !== "object") {
448 throw new TypeError("Immutable#replace can only be invoked with objects or arrays, not " + JSON.stringify(value));
449 }
450
451 return this.merge(value, {deep: deep, mode: 'replace'});
452 }
453
454 var immutableEmptyObject = Immutable({});
455
456 function objectSetIn(path, value, config) {
457 var head = path[0];
458 if (path.length === 1) {
459 return objectSet.call(this, head, value, config);
460 }
461
462 var tail = path.slice(1);
463 var newValue;
464 var thisHead = this[head];
465
466 if (this.hasOwnProperty(head) && typeof(thisHead) === "object" && thisHead !== null && typeof(thisHead.setIn) === "function") {
467 // Might (validly) be object or array
468 newValue = thisHead.setIn(tail, value);
469 } else {
470 newValue = objectSetIn.call(immutableEmptyObject, tail, value);
471 }
472
473 if (this.hasOwnProperty(head) && thisHead === newValue) {
474 return this;
475 }
476
477 var mutable = quickCopy(this, this.instantiateEmptyObject());
478 mutable[head] = newValue;
479 return makeImmutableObject(mutable, this);
480 }
481
482 function objectSet(property, value, config) {
483 var deep = config && config.deep;
484
485 if (this.hasOwnProperty(property)) {
486 if (deep && this[property] !== value && isMergableObject(value) && isMergableObject(this[property])) {
487 value = this[property].merge(value, {deep: true, mode: 'replace'});
488 }
489 if (isEqual(this[property], value)) {
490 return this;
491 }
492 }
493
494 var mutable = quickCopy(this, this.instantiateEmptyObject());
495 mutable[property] = Immutable(value);
496 return makeImmutableObject(mutable, this);
497 }
498
499 function update(property, updater) {
500 var restArgs = Array.prototype.slice.call(arguments, 2);
501 var initialVal = this[property];
502 return this.set(property, updater.apply(initialVal, [initialVal].concat(restArgs)));
503 }
504
505 function getInPath(obj, path) {
506 /*jshint eqnull:true */
507 for (var i = 0, l = path.length; obj != null && i < l; i++) {
508 obj = obj[path[i]];
509 }
510
511 return (i && i == l) ? obj : undefined;
512 }
513
514 function updateIn(path, updater) {
515 var restArgs = Array.prototype.slice.call(arguments, 2);
516 var initialVal = getInPath(this, path);
517
518 return this.setIn(path, updater.apply(initialVal, [initialVal].concat(restArgs)));
519 }
520
521 function asMutableObject(opts) {
522 var result = this.instantiateEmptyObject(), key;
523
524 if(opts && opts.deep) {
525 for (key in this) {
526 if (this.hasOwnProperty(key)) {
527 result[key] = asDeepMutable(this[key]);
528 }
529 }
530 } else {
531 for (key in this) {
532 if (this.hasOwnProperty(key)) {
533 result[key] = this[key];
534 }
535 }
536 }
537
538 return result;
539 }
540
541 // Creates plain object to be used for cloning
542 function instantiatePlainObject() {
543 return {};
544 }
545
546 // Finalizes an object with immutable methods, freezes it, and returns it.
547 function makeImmutableObject(obj, options) {
548 var instantiateEmptyObject =
549 (options && options.instantiateEmptyObject) ?
550 options.instantiateEmptyObject : instantiatePlainObject;
551
552 addPropertyTo(obj, "merge", merge);
553 addPropertyTo(obj, "replace", objectReplace);
554 addPropertyTo(obj, "without", without);
555 addPropertyTo(obj, "asMutable", asMutableObject);
556 addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
557 addPropertyTo(obj, "set", objectSet);
558 addPropertyTo(obj, "setIn", objectSetIn);
559 addPropertyTo(obj, "update", update);
560 addPropertyTo(obj, "updateIn", updateIn);
561
562 return makeImmutable(obj, mutatingObjectMethods);
563 }
564
565 // Returns true if object is a valid react element
566 // https://github.com/facebook/react/blob/v15.0.1/src/isomorphic/classic/element/ReactElement.js#L326
567 function isReactElement(obj) {
568 return typeof obj === 'object' &&
569 obj !== null &&
570 (obj.$$typeof === REACT_ELEMENT_TYPE_FALLBACK || obj.$$typeof === REACT_ELEMENT_TYPE);
571 }
572
573 function Immutable(obj, options, stackRemaining) {
574 if (isImmutable(obj) || isReactElement(obj)) {
575 return obj;
576 } else if (Array.isArray(obj)) {
577 return makeImmutableArray(obj.slice());
578 } else if (obj instanceof Date) {
579 return makeImmutableDate(new Date(obj.getTime()));
580 } else {
581 // Don't freeze the object we were given; make a clone and use that.
582 var prototype = options && options.prototype;
583 var instantiateEmptyObject =
584 (!prototype || prototype === Object.prototype) ?
585 instantiatePlainObject : (function() { return Object.create(prototype); });
586 var clone = instantiateEmptyObject();
587
588 if ("development" !== "production") {
589 /*jshint eqnull:true */
590 if (stackRemaining == null) {
591 stackRemaining = 64;
592 }
593 if (stackRemaining <= 0) {
594 throw new ImmutableError("Attempt to construct Immutable from a deeply nested object was detected." +
595 " Have you tried to wrap an object with circular references (e.g. React element)?" +
596 " See https://github.com/rtfeldman/seamless-immutable/wiki/Deeply-nested-object-was-detected for details.");
597 }
598 stackRemaining -= 1;
599 }
600
601 for (var key in obj) {
602 if (Object.getOwnPropertyDescriptor(obj, key)) {
603 clone[key] = Immutable(obj[key], undefined, stackRemaining);
604 }
605 }
606
607 return makeImmutableObject(clone,
608 {instantiateEmptyObject: instantiateEmptyObject});
609 }
610 }
611
612 // Wrapper to allow the use of object methods as static methods of Immutable.
613 function toStatic(fn) {
614 function staticWrapper() {
615 var args = [].slice.call(arguments);
616 var self = args.shift();
617 return fn.apply(self, args);
618 }
619
620 return staticWrapper;
621 }
622
623 // Wrapper to allow the use of object methods as static methods of Immutable.
624 // with the additional condition of choosing which function to call depending
625 // if argument is an array or an object.
626 function toStaticObjectOrArray(fnObject, fnArray) {
627 function staticWrapper() {
628 var args = [].slice.call(arguments);
629 var self = args.shift();
630 if (Array.isArray(self)) {
631 return fnArray.apply(self, args);
632 } else {
633 return fnObject.apply(self, args);
634 }
635 }
636
637 return staticWrapper;
638 }
639
640 // Export the library
641 Immutable.from = Immutable;
642 Immutable.isImmutable = isImmutable;
643 Immutable.ImmutableError = ImmutableError;
644 Immutable.merge = toStatic(merge);
645 Immutable.replace = toStatic(objectReplace);
646 Immutable.without = toStatic(without);
647 Immutable.asMutable = toStaticObjectOrArray(asMutableObject, asMutableArray);
648 Immutable.set = toStaticObjectOrArray(objectSet, arraySet);
649 Immutable.setIn = toStaticObjectOrArray(objectSetIn, arraySetIn);
650 Immutable.update = toStatic(update);
651 Immutable.updateIn = toStatic(updateIn);
652 Immutable.flatMap = toStatic(flatMap);
653 Immutable.asObject = toStatic(asObject);
654
655 Object.freeze(Immutable);
656
657 /* istanbul ignore if */
658 if (typeof module === "object") {
659 module.exports = Immutable;
660 } else if (typeof exports === "object") {
661 exports.Immutable = Immutable;
662 } else if (typeof window === "object") {
663 window.Immutable = Immutable;
664 } else if (typeof global === "object") {
665 global.Immutable = Immutable;
666 }
667})();