├── src
├── noop.js
├── throttle.js
├── index.js
└── index.test.js
├── .travis.yml
├── .prettierrc
├── bower.json
├── LICENSE
├── .gitignore
├── package.json
├── README.md
├── test.html
└── index.html
/src/noop.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fallback function
3 | * @method noop
4 | * @returns {undefined}
5 | */
6 | export default () => {}
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "10"
5 |
6 | branches:
7 | only:
8 | - master
9 |
10 | cache: yarn
11 |
12 | script: yarn test
13 |
14 | after_success:
15 | - yarn build
16 | - yarn release
17 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": true,
4 | "htmlWhitespaceSensitivity": "css",
5 | "insertPragma": false,
6 | "jsxBracketSameLine": false,
7 | "jsxSingleQuote": false,
8 | "printWidth": 80,
9 | "proseWrap": "preserve",
10 | "quoteProps": "as-needed",
11 | "requirePragma": false,
12 | "semi": false,
13 | "singleQuote": true,
14 | "tabWidth": 2,
15 | "trailingComma": "none",
16 | "useTabs": false
17 | }
18 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "huntjs",
3 | "description": "Minimal library to observe nodes entering and leaving the viewport",
4 | "main": "dist/hunt.js",
5 | "authors": [
6 | "Jeremias Menichelli"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "hunt",
11 | "scroll",
12 | "dom",
13 | "element"
14 | ],
15 | "ignore": [
16 | "src",
17 | "index.html",
18 | "**/.*"
19 | ],
20 | "homepage": "https://github.com/jeremenichelli/hunt",
21 | "moduleType": [
22 | "globals"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/src/throttle.js:
--------------------------------------------------------------------------------
1 | const DEFAULT_THROTTLE_INTERVAL = 100
2 |
3 | /**
4 | * Throttles method execution to prevent some performance bottlenecks
5 | * @param {Function} fn method to throttle
6 | * @param {Number} interval milliseconds for the method to be delayed
7 | */
8 | function throttle(fn, interval = DEFAULT_THROTTLE_INTERVAL) {
9 | let inThrottle
10 | let lastFunc
11 | let lastRan
12 |
13 | return function() {
14 | if (inThrottle === true) {
15 | clearTimeout(lastFunc)
16 | lastFunc = setTimeout(function() {
17 | if (Date.now() - lastRan >= interval) {
18 | fn.apply(this, arguments)
19 | lastRan = Date.now()
20 | }
21 | }, interval - (Date.now() - lastRan))
22 | } else {
23 | fn.apply(this, arguments)
24 | lastRan = Date.now()
25 | inThrottle = true
26 | }
27 | }
28 | }
29 |
30 | export default throttle
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Jeremias Menichelli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # package distribution folder
9 | dist
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "huntjs",
3 | "version": "0.0.0",
4 | "description": "Minimal library to observe nodes entering and leaving the viewport",
5 | "main": "dist/hunt.js",
6 | "browser": "dist/hunt.umd.js",
7 | "module": "dist/hunt.esm.js",
8 | "scripts": {
9 | "format": "prettier ./**/*.js --write",
10 | "test": "ava --verbose",
11 | "prebuild": "npm test",
12 | "start": "microbundle watch -i ./src/index.js --name=Hunt",
13 | "build": "microbundle -i ./src/index.js --name=Hunt --external none",
14 | "release": "travis-deploy-once 'semantic-release'"
15 | },
16 | "files": [
17 | "dist",
18 | "README.md",
19 | "LICENSE"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/jeremenichelli/hunt.git"
24 | },
25 | "keywords": [
26 | "hunt",
27 | "scroll",
28 | "intersection",
29 | "observer",
30 | "viewport",
31 | "dom",
32 | "element"
33 | ],
34 | "author": "Jeremias Menichelli",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/jeremenichelli/hunt/issues"
38 | },
39 | "homepage": "https://github.com/jeremenichelli/hunt#readme",
40 | "devDependencies": {
41 | "@commitlint/cli": "^7.6.1",
42 | "@commitlint/config-conventional": "^7.6.0",
43 | "ava": "^2.0.0",
44 | "esm": "^3.2.25",
45 | "husky": "^2.3.0",
46 | "lint-staged": "^8.1.7",
47 | "lodash.clone": "^4.5.0",
48 | "microbundle": "^0.11.0",
49 | "prettier": "^1.17.1",
50 | "semantic-release": "^15.9.9",
51 | "sinon": "^7.3.2",
52 | "travis-deploy-once": "^5.0.3"
53 | },
54 | "ava": {
55 | "require": [
56 | "esm"
57 | ]
58 | },
59 | "lint-staged": {
60 | "./**/*.js": [
61 | "prettier --write",
62 | "git add"
63 | ]
64 | },
65 | "commitlint": {
66 | "extends": [
67 | "@commitlint/config-conventional"
68 | ]
69 | },
70 | "husky": {
71 | "hooks": {
72 | "pre-commit": "lint-staged && yarn test",
73 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hunt
2 |
3 | [](https://travis-ci.org/jeremenichelli/hunt)
4 |
5 | 👻 Minimal library to observe nodes entering and leaving the viewport.
6 |
7 | _Be sure to also check the **Intersection Observer API**, a native solution which works in modern browsers, if you want to know more I wrote an [introduction article](//jeremenichelli.io/2016/04/quick-introduction-to-the-intersection-observer-api) explaining how it works._
8 |
9 | ## Install
10 |
11 | ```sh
12 | # npm
13 | npm i huntjs --save
14 |
15 | # yarn
16 | yarn add huntjs
17 | ```
18 |
19 | Or include it as a script with `//unpkg.com/huntjs/dist/hunt.umd.js` as source.
20 |
21 | ## Usage
22 |
23 | The package exposes an observer that receives a `Node`, `NodeList` or `Array` as a first argument and an object as a second argument with the desired set of options.
24 |
25 | ```js
26 | import Hunt from 'huntjs';
27 |
28 | // lazy loading images using dataset and hunt
29 | const lazyImages = document.querySelectorAll('img.lazy');
30 |
31 | let observer = new Hunt(lazyImages, {
32 | enter: (image) => image.src = image.dataset.src
33 | });
34 | ```
35 |
36 | _Check this example working [here](//jeremenichelli.github.io/hunt)_
37 |
38 | By default the observer will stop _hunting_ elements when they enter and then leave the viewport.
39 |
40 | ## Config options
41 |
42 | These are the properties you can set as a configuration:
43 |
44 | - `enter`, _function_ called when the element becomes visible.
45 | - `leave`, _function_ method called when the element leaves completely the viewport.
46 |
47 | _Both callbacks will receive the element which triggered the action as argument._
48 |
49 | - `persist`, _boolean_ and `false` by default which indicates if the targets should still be observed after they entered and left the viewport. When this option is `true` enter and leave methods will be called each time an element state changes. Recommended for constant animations triggered by scroll events.
50 | - `offset`, _number_ that defines a number of pixels ahead of the element's state change, `0` being the default value. Good if you want to start loading an image before the user reaches to it.
51 | - `throttleInterval`, _number_ interval use for event throttling. A lower number will mean elements are detected in view quicker, but may degrade performace. A higher value will mean elements are detected in view slower, but may improve performance. The default value is `100`, is recommended not to modify this.
52 |
53 | ### Custom configuration over dataset
54 |
55 | If you need exceptions over config for one or more elements, `data-hunt-offset` and `data-hunt-persist` attributes can be used. These custom values will override the ones you passed to the observer.
56 |
57 | ```html
58 |
63 |
64 | ```
65 |
66 | _**JSON.parse** is used on these values at runtime, make sure to pass safe content._
67 |
68 | ## Contributing
69 |
70 | To contribute [Node.js](//nodejs.org) and [yarn](//yarnpkg.com) are required.
71 |
72 | Before commit make sure to follow [conventional commits](//www.conventionalcommits.org) specification and check all tests pass by running `yarn test`.
73 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | hunt | stress test
7 |
8 |
9 |
60 |
61 |
62 |
70 |
71 |
72 |
73 |
74 |
75 |
87 |
88 |
89 |
90 |
91 |
92 |
103 |
104 |
105 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | hunt | lazy loading image example
10 |
11 |
12 |
13 |
72 |
73 |
74 |
81 |
82 |
83 |
import Hunt from 'huntjs';
84 |
85 | // lazy loading images using dataset and hunt
86 | const lazyImages = document.querySelectorAll('img.lazy');
87 |
88 | let observer = new Hunt(lazyImages, {
89 | enter: (image) => image.src = image.dataset.src
90 | });
91 |
92 |
![]()
93 |
![]()
94 |
![]()
95 |
![]()
96 |
![]()
97 |
![]()
98 |
![]()
99 |
![]()
100 |
![]()
101 |
![]()
102 |
103 |
104 |
105 |
106 |
112 |
113 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import noop from './noop'
2 | import throttle from './throttle'
3 |
4 | /**
5 | * Constructor for element that should be hunted
6 | * @constructor Hunted
7 | * @param {Node} element
8 | * @param {Object} config
9 | */
10 | class Hunted {
11 | constructor(element, config) {
12 | this.element = element
13 |
14 | // instantiate element as not visible
15 | this.visible = false
16 |
17 | // extend properties from config or fallback to prototype values
18 | for (var prop in config) {
19 | if (Object.hasOwnProperty.call(config, prop)) {
20 | this[prop] = config[prop]
21 | }
22 | }
23 |
24 | // replace options with dataset if present
25 | if (typeof element.dataset !== 'undefined') {
26 | if (typeof element.dataset.huntPersist !== 'undefined') {
27 | try {
28 | this.persist = JSON.parse(element.dataset.huntPersist)
29 | } catch (e) {}
30 | }
31 | if (typeof element.dataset.huntOffset !== 'undefined') {
32 | try {
33 | this.offset = JSON.parse(element.dataset.huntOffset)
34 | } catch (e) {}
35 | }
36 | }
37 | }
38 | }
39 |
40 | // protoype values
41 | Hunted.prototype.offset = 0
42 | Hunted.prototype.persist = false
43 | Hunted.prototype.enter = noop
44 | Hunted.prototype.leave = noop
45 |
46 | /**
47 | * Creates and initializes observer
48 | * @constructor Hunt
49 | * @param {Node|NodeList|Array} target
50 | * @param {Object} options
51 | */
52 | class Hunt {
53 | constructor(target, options) {
54 | // sanity check for first argument
55 | const isValidTarget =
56 | (target && target.nodeType === 1) || typeof target.length === 'number'
57 | if (!isValidTarget) {
58 | throw new TypeError(
59 | 'hunt: observer first argument should be a node or a list of nodes'
60 | )
61 | }
62 | // sanity check for second argument
63 | if (typeof options !== 'object') {
64 | throw new TypeError('hunt: observer second argument should be an object')
65 | }
66 |
67 | // turn target to array
68 | if (target.nodeType === 1) {
69 | this.__huntedElements__ = [new Hunted(target, options)]
70 | } else {
71 | const targetArray = [].slice.call(target)
72 | this.__huntedElements__ = targetArray.map((t) => new Hunted(t, options))
73 | }
74 |
75 | // hoist viewport metrics
76 | this.__viewportHeight__ = window.innerHeight
77 |
78 | // connect observer and pass in throttle interval
79 | this.__connect__(options.throttleInterval)
80 | }
81 |
82 | /**
83 | * Assign throttled actions and add listeners
84 | * @param {Number} throttleInterval
85 | * @method __connect__
86 | * @memberof Hunt
87 | */
88 | __connect__(throttleInterval) {
89 | // throttle actions
90 | this.__throttledHuntElements__ = throttle(
91 | this.__huntElements__.bind(this),
92 | throttleInterval
93 | )
94 | this.__throttledUpdateMetrics__ = throttle(
95 | this.__updateMetrics__.bind(this),
96 | throttleInterval
97 | )
98 |
99 | // add listeners
100 | window.addEventListener('scroll', this.__throttledHuntElements__)
101 | window.addEventListener('resize', this.__throttledUpdateMetrics__)
102 |
103 | // run first check
104 | this.__huntElements__()
105 | }
106 |
107 | /**
108 | * Checks if hunted elements are visible and apply callbacks
109 | * @method __huntElements__
110 | * @memberof Hunt
111 | */
112 | __huntElements__() {
113 | let position = this.__huntedElements__.length
114 |
115 | while (position) {
116 | --position
117 | const hunted = this.__huntedElements__[position]
118 | const rect = hunted.element.getBoundingClientRect()
119 | const isOnViewport =
120 | rect.top - hunted.offset < this.__viewportHeight__ &&
121 | rect.top >= -(rect.height + hunted.offset)
122 |
123 | /*
124 | * trigger (enter) event if element comes from a non visible state and the scrolled
125 | * viewport has reached the visible range of the element without exceeding it
126 | */
127 | if (hunted.visible === false && isOnViewport === true) {
128 | hunted.enter.call(null, hunted.element)
129 | hunted.visible = true
130 | // when the leave callback method is not set and hunting should not persist remove element
131 | if (hunted.leave === noop && hunted.persist !== true) {
132 | this.__huntedElements__.splice(position, 1)
133 |
134 | // end observer activity when there are no more elements
135 | if (this.__huntedElements__.length === 0) {
136 | this.disconnect()
137 | }
138 | }
139 | }
140 |
141 | /*
142 | * trigger (leave) event if element comes from a visible state
143 | * and it's out of the visible range its bottom or top limit
144 | */
145 | if (hunted.visible === true && isOnViewport === false) {
146 | hunted.leave.call(null, hunted.element)
147 | hunted.visible = false
148 | // when hunting should not persist remove element
149 | if (hunted.persist !== true) {
150 | this.__huntedElements__.splice(position, 1)
151 |
152 | // end observer activity when there are no more elements
153 | if (this.__huntedElements__.length === 0) {
154 | this.disconnect()
155 | }
156 | }
157 | }
158 | }
159 | }
160 |
161 | /**
162 | * Update viewport tracked height and runs a check
163 | * @method __updateMetrics__
164 | * @memberof Hunt
165 | */
166 | __updateMetrics__() {
167 | this.__viewportHeight__ = window.innerHeight
168 | this.__huntElements__()
169 | }
170 |
171 | /**
172 | * Remove listeners and stops observing elements
173 | * @method disconnect
174 | * @memberof Hunt
175 | */
176 | disconnect() {
177 | // remove listeners
178 | window.removeEventListener('scroll', this.__throttledHuntElements__)
179 | window.removeEventListener('resize', this.__throttledUpdateMetrics__)
180 | }
181 |
182 | /**
183 | * __huntElements__ public alias
184 | * @method trigger
185 | * @memberof Hunt
186 | */
187 | trigger() {
188 | this.__huntElements__()
189 | }
190 | }
191 |
192 | export default Hunt
193 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import Hunt from '.'
2 | import test from 'ava'
3 | import sinon from 'sinon'
4 | import clone from 'lodash.clone'
5 |
6 | // mock window object
7 | const window = {
8 | innerHeight: 0,
9 | addEventListener() {},
10 | removeEventListener() {}
11 | }
12 |
13 | test.beforeEach(() => {
14 | global.window = clone(window)
15 | })
16 |
17 | test('calls enter and leave hooks correctly and persists', (t) => {
18 | // alter window dimensions
19 | global.window.innerHeight = 550
20 |
21 | // fake rects
22 | let rects = {
23 | x: 352,
24 | y: 1040,
25 | width: 350,
26 | height: 350,
27 | top: 1040,
28 | right: 702,
29 | bottom: 1390,
30 | left: 352
31 | }
32 |
33 | // mock element outside the viewport
34 | const mockedElement = {
35 | nodeType: 1,
36 | getBoundingClientRect() {
37 | return rects
38 | }
39 | }
40 |
41 | const enterSpy = sinon.spy()
42 | const leaveSpy = sinon.spy()
43 | const options = {
44 | enter: enterSpy,
45 | leave: leaveSpy,
46 | persist: true
47 | }
48 |
49 | const observer = new Hunt(mockedElement, options)
50 | t.is(enterSpy.callCount, 0)
51 | t.is(leaveSpy.callCount, 0)
52 |
53 | // modify rects for viewport visibility and trigger observer
54 | rects = {
55 | x: 352,
56 | y: 97,
57 | width: 350,
58 | height: 350,
59 | top: 97,
60 | right: 702,
61 | bottom: 447,
62 | left: 352
63 | }
64 | observer.trigger()
65 |
66 | t.is(enterSpy.callCount, 1)
67 | t.is(leaveSpy.callCount, 0)
68 |
69 | // modify rects for viewport leave and trigger observer
70 | rects = {
71 | x: 352,
72 | y: 1040,
73 | width: 350,
74 | height: 350,
75 | top: 1040,
76 | right: 702,
77 | bottom: 1390,
78 | left: 352
79 | }
80 | observer.trigger()
81 |
82 | t.is(enterSpy.callCount, 1)
83 | t.is(leaveSpy.callCount, 1)
84 |
85 | // modify rects for viewport visibility and trigger observer
86 | rects = {
87 | x: 352,
88 | y: 97,
89 | width: 350,
90 | height: 350,
91 | top: 97,
92 | right: 702,
93 | bottom: 447,
94 | left: 352
95 | }
96 | observer.trigger()
97 |
98 | t.is(enterSpy.callCount, 2)
99 | t.is(leaveSpy.callCount, 1)
100 | })
101 |
102 | test('disconnects when persist is false', (t) => {
103 | // alter window dimensions
104 | global.window.innerHeight = 550
105 |
106 | // fake rects
107 | let rects = {
108 | x: 352,
109 | y: 1040,
110 | width: 350,
111 | height: 350,
112 | top: 1040,
113 | right: 702,
114 | bottom: 1390,
115 | left: 352
116 | }
117 |
118 | // mock element outside the viewport
119 | const mockedElement = {
120 | nodeType: 1,
121 | getBoundingClientRect() {
122 | return rects
123 | }
124 | }
125 |
126 | const disconnectSpy = sinon.spy()
127 | const enterSpy = sinon.spy()
128 | const leaveSpy = sinon.spy()
129 | const options = {
130 | enter: enterSpy,
131 | leave: leaveSpy
132 | }
133 |
134 | const observer = new Hunt(mockedElement, options)
135 | observer.disconnect = disconnectSpy
136 | t.is(enterSpy.callCount, 0)
137 | t.is(leaveSpy.callCount, 0)
138 | t.is(disconnectSpy.callCount, 0)
139 |
140 | // modify rects for viewport visibility and trigger observer
141 | rects = {
142 | x: 352,
143 | y: 97,
144 | width: 350,
145 | height: 350,
146 | top: 97,
147 | right: 702,
148 | bottom: 447,
149 | left: 352
150 | }
151 | observer.trigger()
152 |
153 | t.is(enterSpy.callCount, 1)
154 | t.is(leaveSpy.callCount, 0)
155 | t.is(disconnectSpy.callCount, 0)
156 |
157 | // modify rects for viewport leave and trigger observer
158 | rects = {
159 | x: 352,
160 | y: 1040,
161 | width: 350,
162 | height: 350,
163 | top: 1040,
164 | right: 702,
165 | bottom: 1390,
166 | left: 352
167 | }
168 | observer.trigger()
169 |
170 | t.is(enterSpy.callCount, 1)
171 | t.is(leaveSpy.callCount, 1)
172 | t.is(disconnectSpy.callCount, 1)
173 | })
174 |
175 | test('accepts collection of elements', (t) => {
176 | // alter window dimensions
177 | global.window.innerHeight = 550
178 |
179 | // fake rects
180 | let firstElementRect = {
181 | x: 352,
182 | y: 1040,
183 | width: 350,
184 | height: 350,
185 | top: 1040,
186 | right: 702,
187 | bottom: 1390,
188 | left: 352
189 | }
190 | let secondElementRect = {
191 | x: 737,
192 | y: 1429,
193 | width: 350,
194 | height: 350,
195 | top: 1429,
196 | right: 1087,
197 | bottom: 1779,
198 | left: 737
199 | }
200 |
201 | // mock element outside the viewport
202 | const firstMockedElement = {
203 | getBoundingClientRect() {
204 | return firstElementRect
205 | }
206 | }
207 | const secondMockedElement = {
208 | getBoundingClientRect() {
209 | return secondElementRect
210 | }
211 | }
212 |
213 | const enterSpy = sinon.spy()
214 | const leaveSpy = sinon.spy()
215 | const options = {
216 | enter: enterSpy,
217 | leave: leaveSpy,
218 | persist: true
219 | }
220 |
221 | const observer = new Hunt([firstMockedElement, secondMockedElement], options)
222 |
223 | // modify rects for viewport visibility and trigger observer
224 | firstElementRect = {
225 | x: 352,
226 | y: -90,
227 | width: 350,
228 | height: 350,
229 | top: -90,
230 | right: 702,
231 | bottom: 259,
232 | left: 352
233 | }
234 | secondElementRect = {
235 | x: 737,
236 | y: 298,
237 | width: 350,
238 | height: 350,
239 | top: 298,
240 | right: 1087,
241 | bottom: 648,
242 | left: 737
243 | }
244 | observer.trigger()
245 |
246 | // hunt does reverse crawling on elements when they get to the viewport
247 | t.deepEqual(enterSpy.getCall(1).args[0], firstMockedElement)
248 | t.deepEqual(enterSpy.getCall(0).args[0], secondMockedElement)
249 |
250 | // modify rects for viewport leave and trigger observer
251 | firstElementRect = {
252 | x: 352,
253 | y: 1040,
254 | width: 350,
255 | height: 350,
256 | top: 1040,
257 | right: 702,
258 | bottom: 1390,
259 | left: 352
260 | }
261 | secondElementRect = {
262 | x: 737,
263 | y: 1429,
264 | width: 350,
265 | height: 350,
266 | top: 1429,
267 | right: 1087,
268 | bottom: 1779,
269 | left: 737
270 | }
271 | observer.trigger()
272 |
273 | // hunt does reverse crawling on elements when they get to the viewport
274 | t.deepEqual(leaveSpy.getCall(1).args[0], firstMockedElement)
275 | t.deepEqual(leaveSpy.getCall(0).args[0], secondMockedElement)
276 | })
277 |
278 | test('adds event listeners on instantiation and removes them on disconnect', (t) => {
279 | global.window.addEventListener = sinon.spy()
280 | global.window.removeEventListener = sinon.spy()
281 |
282 | // fake rects
283 | let rects = {
284 | x: 352,
285 | y: 1040,
286 | width: 350,
287 | height: 350,
288 | top: 1040,
289 | right: 702,
290 | bottom: 1390,
291 | left: 352
292 | }
293 |
294 | // mock element outside the viewport
295 | const mockedElement = {
296 | nodeType: 1,
297 | getBoundingClientRect() {
298 | return rects
299 | }
300 | }
301 |
302 | const observer = new Hunt(mockedElement, {})
303 |
304 | t.is(global.window.addEventListener.getCall(0).args[0], 'scroll')
305 | t.is(global.window.addEventListener.getCall(1).args[0], 'resize')
306 |
307 | observer.disconnect()
308 |
309 | t.is(global.window.removeEventListener.getCall(0).args[0], 'scroll')
310 | t.is(global.window.removeEventListener.getCall(1).args[0], 'resize')
311 | })
312 |
313 | test('throws when call with wrong arguments', (t) => {
314 | // called with an invalid target
315 | t.throws(
316 | () => new Hunt({}),
317 | 'hunt: observer first argument should be a node or a list of nodes'
318 | )
319 |
320 | // called with an invalid target
321 | t.throws(
322 | () => new Hunt({ nodeType: 1 }),
323 | 'hunt: observer second argument should be an object'
324 | )
325 | })
326 |
--------------------------------------------------------------------------------