Skip to content

Commit 77019bc

Browse files
author
Amir Tocker
committed
Refactor utility methods.
* Replace compareObject with equals and move to Util * Move isElement and isObjectLike to Util * Add Util tests
1 parent 074cf64 commit 77019bc

File tree

6 files changed

+797
-39
lines changed

6 files changed

+797
-39
lines changed

src/Util/Util.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export debounce from './debounce';
22
export firstDefined from './firstDefined';
33
export closestAbove from './closestAbove';
4-
export {requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame';
4+
export {requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame';
5+
export equals from './equals';
6+
export isElement from './isElement';

src/Util/equals.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
Source: https://github.com/ReactiveSets/toubkal
3+
4+
The MIT License (MIT)
5+
6+
Copyright (c) 2013-2016, Reactive Sets
7+
8+
Permission is hereby granted, free of charge, to any person obtaining a copy
9+
of this software and associated documentation files (the "Software"), to deal
10+
in the Software without restriction, including without limitation the rights
11+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
copies of the Software, and to permit persons to whom the Software is
13+
furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in all
16+
copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.
25+
*/
26+
'use strict';
27+
28+
var toString = Object.prototype.toString;
29+
30+
/* -----------------------------------------------------------------------------------------
31+
equals( a, b [, enforce_properties_order, cyclic] )
32+
33+
Returns true if a and b are deeply equal, false otherwise.
34+
35+
Parameters:
36+
- a (Any type): value to compare to b
37+
- b (Any type): value compared to a
38+
39+
Optional Parameters:
40+
- enforce_properties_order (Boolean): true to check if Object properties are provided
41+
in the same order between a and b
42+
43+
- cyclic (Boolean): true to check for cycles in cyclic objects
44+
45+
Implementation:
46+
'a' is considered equal to 'b' if all scalar values in a and b are strictly equal as
47+
compared with operator '===' except for these two special cases:
48+
- 0 === -0 but are not equal.
49+
- NaN is not === to itself but is equal.
50+
51+
RegExp objects are considered equal if they have the same lastIndex, i.e. both regular
52+
expressions have matched the same number of times.
53+
54+
Functions must be identical, so that they have the same closure context.
55+
56+
"undefined" is a valid value, including in Objects
57+
58+
106 automated tests.
59+
60+
Provide options for slower, less-common use cases:
61+
62+
- Unless enforce_properties_order is true, if 'a' and 'b' are non-Array Objects, the
63+
order of occurence of their attributes is considered irrelevant:
64+
{ a: 1, b: 2 } is considered equal to { b: 2, a: 1 }
65+
66+
- Unless cyclic is true, Cyclic objects will throw:
67+
RangeError: Maximum call stack size exceeded
68+
*/
69+
export default function equals(a, b, enforce_properties_order, cyclic) {
70+
return a === b // strick equality should be enough unless zero
71+
&& a !== 0 // because 0 === -0, requires test by _equals()
72+
|| _equals(a, b) // handles not strictly equal or zero values
73+
;
74+
75+
function _equals(a, b) {
76+
// a and b have already failed test for strict equality or are zero
77+
78+
var s, l, p, x, y;
79+
80+
// They should have the same toString() signature
81+
if (( s = toString.call(a) ) !== toString.call(b)) return false;
82+
83+
switch (s) {
84+
default: // Boolean, Date, String
85+
return a.valueOf() === b.valueOf();
86+
87+
case '[object Number]':
88+
// Converts Number instances into primitive values
89+
// This is required also for NaN test bellow
90+
a = +a;
91+
b = +b;
92+
93+
return a ? // a is Non-zero and Non-NaN
94+
a === b
95+
: // a is 0, -0 or NaN
96+
a === a ? // a is 0 or -O
97+
1 / a === 1 / b // 1/0 !== 1/-0 because Infinity !== -Infinity
98+
: b !== b // NaN, the only Number not equal to itself!
99+
;
100+
// [object Number]
101+
102+
case '[object RegExp]':
103+
return a.source == b.source
104+
&& a.global == b.global
105+
&& a.ignoreCase == b.ignoreCase
106+
&& a.multiline == b.multiline
107+
&& a.lastIndex == b.lastIndex
108+
;
109+
// [object RegExp]
110+
111+
case '[object Function]':
112+
return false; // functions should be strictly equal because of closure context
113+
// [object Function]
114+
115+
case '[object Array]':
116+
if (cyclic && ( x = reference_equals(a, b) ) !== null) return x; // intentionally duplicated bellow for [object Object]
117+
118+
if (( l = a.length ) != b.length) return false;
119+
// Both have as many elements
120+
121+
while (l--) {
122+
if (( x = a[l] ) === ( y = b[l] ) && x !== 0 || _equals(x, y)) continue;
123+
124+
return false;
125+
}
126+
127+
return true;
128+
// [object Array]
129+
130+
case '[object Object]':
131+
if (cyclic && ( x = reference_equals(a, b) ) !== null) return x; // intentionally duplicated from above for [object Array]
132+
133+
l = 0; // counter of own properties
134+
135+
if (enforce_properties_order) {
136+
var properties = [];
137+
138+
for (p in a) {
139+
if (a.hasOwnProperty(p)) {
140+
properties.push(p);
141+
142+
if (( x = a[p] ) === ( y = b[p] ) && x !== 0 || _equals(x, y)) continue;
143+
144+
return false;
145+
}
146+
}
147+
148+
// Check if 'b' has as the same properties as 'a' in the same order
149+
for (p in b)
150+
if (b.hasOwnProperty(p) && properties[l++] != p)
151+
return false;
152+
} else {
153+
for (p in a) {
154+
if (a.hasOwnProperty(p)) {
155+
++l;
156+
157+
if (( x = a[p] ) === ( y = b[p] ) && x !== 0 || _equals(x, y)) continue;
158+
159+
return false;
160+
}
161+
}
162+
163+
// Check if 'b' has as not more own properties than 'a'
164+
for (p in b)
165+
if (b.hasOwnProperty(p) && --l < 0)
166+
return false;
167+
}
168+
169+
return true;
170+
// [object Object]
171+
} // switch toString.call( a )
172+
} // _equals()
173+
174+
/* -----------------------------------------------------------------------------------------
175+
reference_equals( a, b )
176+
177+
Helper function to compare object references on cyclic objects or arrays.
178+
179+
Returns:
180+
- null if a or b is not part of a cycle, adding them to object_references array
181+
- true: same cycle found for a and b
182+
- false: different cycle found for a and b
183+
184+
On the first call of a specific invocation of equal(), replaces self with inner function
185+
holding object_references array object in closure context.
186+
187+
This allows to create a context only if and when an invocation of equal() compares
188+
objects or arrays.
189+
*/
190+
function reference_equals(a, b) {
191+
var object_references = [];
192+
193+
return ( reference_equals = _reference_equals )(a, b);
194+
195+
function _reference_equals(a, b) {
196+
var l = object_references.length;
197+
198+
while (l--)
199+
if (object_references[l--] === b)
200+
return object_references[l] === a;
201+
202+
object_references.push(a, b);
203+
204+
return null;
205+
} // _reference_equals()
206+
} // reference_equals()
207+
} // equals()

src/Util/isElement.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Originally from lodash
2+
3+
/**
4+
* Checks if `value` is a DOM element.
5+
*
6+
* @static
7+
* @memberOf _
8+
* @category Lang
9+
* @param {*} value The value to check.
10+
* @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.
11+
* @example
12+
*
13+
* _.isElement(document.body);
14+
* // => true
15+
*
16+
* _.isElement('<body>');
17+
* // => false
18+
*/
19+
export default function isElement(value) {
20+
return value != null && value.nodeType === 1 && isObjectLike(value);
21+
}
22+
23+
function isObjectLike(value) {
24+
return value != null && typeof value == 'object';
25+
}
26+
27+

src/components/Image/Image.js

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,11 @@
11
import React, {Component, PropTypes} from 'react';
22
import cloudinary, {Util} from 'cloudinary-core';
33
import CloudinaryComponent from '../CloudinaryComponent';
4-
import {debounce, firstDefined, closestAbove, requestAnimationFrame} from '../../Util';
5-
6-
function compareObjects(o, p) {
7-
let i,
8-
keysO = Object.keys(o).sort(),
9-
keysP = Object.keys(p).sort();
10-
if (keysO.length !== keysP.length) return false;
11-
if (keysO.join('') !== keysP.join('')) return false;
12-
for (i = 0; i < keysO.length; ++i) {
13-
if (o[keysO[i]] instanceof Array) {
14-
if (!(p[keysO[i]] instanceof Array)) return false;
15-
if (p[keysO[i]].sort().join('') !== o[keysO[i]].sort().join('')) return false;
16-
}
17-
else if (o[keysO[i]] instanceof Function) {
18-
if (!(p[keysO[i]] instanceof Function)) return false;
19-
}
20-
else if (o[keysO[i]] instanceof Object) {
21-
if (!(p[keysO[i]] instanceof Object)) return false;
22-
if (o[keysO[i]] === o) {
23-
if (p[keysO[i]] !== p) return false;
24-
} else if (compareObjects(o[keysO[i]], p[keysO[i]]) === false) {
25-
return false;//WARNING: does not deal with circular refs other than ^^
26-
}
27-
}
28-
if (o[keysO[i]] != p[keysO[i]]) return false;//not the same value
29-
}
30-
return true;
31-
}
32-
33-
function isElement(value) {
34-
return value != null && value.nodeType === 1 && isObjectLike(value);
35-
}
36-
37-
function isObjectLike(value) {
38-
return value != null && typeof value == 'object';
39-
}
4+
import {debounce, firstDefined, closestAbove, requestAnimationFrame, equals, isElement} from '../../Util';
405

6+
/**
7+
* An element representing a Cloudinary served image
8+
*/
419
export default class Image extends CloudinaryComponent {
4210
constructor(props, context) {
4311
function defaultBreakpoints(width, steps = 100) {
@@ -51,22 +19,33 @@ export default class Image extends CloudinaryComponent {
5119
this.state = Object.assign(state, this.prepareState(props, context));
5220
}
5321

22+
/**
23+
* Retrieve the window or default view of the current element
24+
* @returns {DocumentView|*}
25+
*/
5426
get window() {
5527
let windowRef = null;
5628
if(typeof window !== "undefined"){
5729
windowRef = window
5830
}
5931
return (this.element && this.element.ownerDocument) ? (this.element.ownerDocument.defaultView || windowRef) : windowRef;
6032
}
33+
6134
shouldComponentUpdate( nextProps, nextState){
62-
return !( compareObjects(this.props, nextProps) && compareObjects(this.state, nextState));
35+
return !( equals(this.props, nextProps) && equals(this.state, nextState));
6336
}
6437

6538
componentWillReceiveProps(nextProps, nextContext) {
6639
let state = this.prepareState(nextProps, nextContext);
6740
this.setState(state);
6841
}
6942

43+
/**
44+
* Generate update state of this element
45+
* @param {Object} [props=this.props]
46+
* @param {Object} [context=this.context]
47+
* @returns {Object} state updates
48+
*/
7049
prepareState(props = this.props, context = this.context) {
7150
let options = CloudinaryComponent.normalizeOptions(context, props);
7251
let url = this.getUrl(options);
@@ -238,4 +217,3 @@ export default class Image extends CloudinaryComponent {
238217
Image.defaultProps = {};
239218
Image.contextTypes = CloudinaryComponent.contextTypes;
240219
Image.propTypes = CloudinaryComponent.propTypes;
241-

test/Util.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {expect} from 'chai';
2+
import {firstDefined, closestAbove} from '../src/Util';
3+
4+
describe('Util', () => {
5+
describe('firstDefined', () => {
6+
it("should return the first argument that is defined", function () {
7+
8+
expect(firstDefined(1,2)).to.equal(1);
9+
expect(firstDefined(0,1,2)).to.equal(0);
10+
expect(firstDefined(undefined,1,2)).to.equal(1);
11+
expect(firstDefined(undefined,undefined,2)).to.equal(2);
12+
expect(firstDefined(undefined,undefined,undefined)).to.equal(undefined);
13+
expect(firstDefined(undefined,undefined,null)).to.equal(null);
14+
expect(firstDefined('1','2')).to.equal('1');
15+
expect(firstDefined('0','1','2')).to.equal('0');
16+
expect(firstDefined(undefined,'1','2')).to.equal('1');
17+
expect(firstDefined(undefined,undefined,'2')).to.equal('2');
18+
expect(firstDefined(undefined,'undefined',undefined)).to.equal('undefined');
19+
expect(firstDefined(undefined,undefined,'null')).to.equal('null');
20+
});
21+
});
22+
describe('closestAbove()', () => {
23+
it('should return the first item in list that is greater or equal to the given value', () => {
24+
expect(closestAbove([1,10,100], 5)).to.equal(10);
25+
expect(closestAbove([1,10,100], undefined)).to.equal(100);
26+
expect(closestAbove([1,10,100], 500)).to.equal(100);
27+
expect(closestAbove([100,10,1], 5)).to.equal(100);
28+
expect(closestAbove([], 5)).to.equal(undefined);
29+
})
30+
});
31+
});

0 commit comments

Comments
 (0)