├── app ├── .gitkeep ├── services │ └── in-viewport.js ├── modifiers │ └── in-viewport.js └── initializers │ └── viewport-config.js ├── addon ├── .gitkeep ├── utils │ ├── can-use-dom.js │ ├── find-elem.js │ ├── is-in-viewport.js │ ├── check-scroll-direction.js │ ├── can-use-raf.js │ └── can-use-intersection-observer.js ├── breakpoints.js ├── initializers │ └── viewport-config.js ├── -private │ ├── observer-admin.js │ └── raf-admin.js ├── modifiers │ └── in-viewport.js └── services │ └── in-viewport.js ├── vendor └── .gitkeep ├── tests ├── unit │ ├── .gitkeep │ ├── initializers │ │ └── viewport-config-test.js │ ├── services │ │ └── -observer-admin-test.js │ ├── raf-pool-test.js │ └── utils │ │ ├── check-scroll-direction-test.js │ │ └── is-in-viewport-test.js ├── integration │ ├── .gitkeep │ └── components │ │ ├── my-component-test.js │ │ └── modifier-test.js ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ ├── infinity.js │ │ │ ├── infinity-class.js │ │ │ ├── infinity-modifier.js │ │ │ ├── infinity-custom-element.js │ │ │ ├── infinity-scrollable-raf.js │ │ │ ├── infinity-scrollable-scrollevent.js │ │ │ ├── infinity-right-left.js │ │ │ └── infinity-scrollable.js │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── my-class-raf.js │ │ │ ├── my-class.js │ │ │ ├── my-modifier.js │ │ │ ├── my-component.js │ │ │ └── dummy-artwork.js │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ ├── infinity-scrollable-scrollevent.js │ │ │ ├── infinity-right-left.js │ │ │ ├── infinity-scrollable.js │ │ │ ├── infinity-scrollable-raf.js │ │ │ ├── infinity-class.js │ │ │ ├── infinity-modifier.js │ │ │ ├── infinity.js │ │ │ ├── infinity-built-in-modifiers.js │ │ │ └── infinity-custom-element.js │ │ ├── templates │ │ │ ├── components │ │ │ │ ├── .gitkeep │ │ │ │ ├── my-class.hbs │ │ │ │ ├── my-class-raf.hbs │ │ │ │ ├── my-modifier.hbs │ │ │ │ ├── my-component.hbs │ │ │ │ └── dummy-artwork.hbs │ │ │ ├── infinity-custom-element.hbs │ │ │ ├── infinity-modifier.hbs │ │ │ ├── infinity.hbs │ │ │ ├── index.hbs │ │ │ ├── infinity-scrollable.hbs │ │ │ ├── infinity-scrollable-scrollevent.hbs │ │ │ ├── infinity-right-left.hbs │ │ │ ├── infinity-class.hbs │ │ │ ├── application.hbs │ │ │ ├── infinity-scrollable-raf.hbs │ │ │ └── infinity-built-in-modifiers.hbs │ │ ├── resolver.js │ │ ├── app.js │ │ ├── router.js │ │ ├── index.html │ │ ├── styles │ │ │ └── app.css │ │ └── -config │ │ │ └── index.js │ ├── public │ │ ├── robots.txt │ │ ├── assets │ │ │ ├── 1x1.gif │ │ │ └── missing-artwork.svg │ │ └── crossdomain.xml │ └── config │ │ ├── optional-features.json │ │ ├── ember-cli-update.json │ │ ├── targets.js │ │ └── environment.js ├── helpers │ ├── destroy-app.js │ ├── resolver.js │ └── start-app.js ├── test-helper.js ├── index.html └── acceptance │ ├── integration-test.js │ └── infinity-test.js ├── .watchmanconfig ├── .prettierrc.js ├── index.js ├── config ├── environment.js └── ember-try.js ├── .template-lintrc.js ├── .ember-cli ├── .prettierignore ├── .eslintignore ├── .release-it.json ├── .editorconfig ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── testem.js ├── .npmignore ├── ember-cli-build.js ├── LICENSE.md ├── .eslintrc.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json ├── README.md └── CHANGELOG.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/services/in-viewport.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-in-viewport/services/in-viewport'; 2 | -------------------------------------------------------------------------------- /app/modifiers/in-viewport.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-in-viewport/modifiers/in-viewport'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/my-class.hbs: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/my-class-raf.hbs: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-class.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/my-modifier.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{yield}} 3 |
4 | -------------------------------------------------------------------------------- /tests/dummy/public/assets/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/ember-in-viewport/HEAD/tests/dummy/public/assets/1x1.gif -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-modifier.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-custom-element.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend(); 4 | -------------------------------------------------------------------------------- /app/initializers/viewport-config.js: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | initialize, 4 | } from 'ember-in-viewport/initializers/viewport-config'; 5 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-scrollable-raf.js: -------------------------------------------------------------------------------- 1 | import ScrollableRoute from './infinity-scrollable'; 2 | 3 | export default ScrollableRoute.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-scrollable-scrollevent.js: -------------------------------------------------------------------------------- 1 | import ScrollableRoute from './infinity-scrollable'; 2 | 3 | export default ScrollableRoute.extend({}); 4 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | 3 | export default function destroyApp(application) { 4 | run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /addon/utils/can-use-dom.js: -------------------------------------------------------------------------------- 1 | const canUseDOM = !!( 2 | typeof window !== 'undefined' && 3 | window.document && 4 | window.document.createElement 5 | ); 6 | 7 | export default canUseDOM; 8 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane', 5 | rules: { 6 | 'no-triple-curlies': false, 7 | 'no-inline-styles': false, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /addon/breakpoints.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mobile: '(max-width: 767px)', 3 | tablet: '(min-width: 768px) and (max-width: 991px)', 4 | desktop: '(min-width: 992px) and (max-width: 1200px)', 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-scrollable-scrollevent.js: -------------------------------------------------------------------------------- 1 | import InfinityScrollable from './infinity-scrollable'; 2 | 3 | export default class InfinityScrollableScrollevent extends InfinityScrollable {} 4 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/my-component.hbs: -------------------------------------------------------------------------------- 1 |
6 | {{yield}} 7 |
-------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-custom-element.hbs: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /addon/utils/find-elem.js: -------------------------------------------------------------------------------- 1 | export default function (context) { 2 | let elem; 3 | if ( 4 | context.nodeType === Node.ELEMENT_NODE || 5 | context.nodeType === Node.DOCUMENT_NODE || 6 | context instanceof Window 7 | ) { 8 | elem = context; 9 | } else { 10 | elem = document.querySelector(context); 11 | } 12 | 13 | return elem; 14 | } 15 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-modifier.hbs: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity.hbs: -------------------------------------------------------------------------------- 1 | 6 | 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "tagName": "v${version}", 4 | "commitMessage": "v${version}", 5 | "pushArgs": "--follow-tags" 6 | }, 7 | 8 | "npm": { 9 | "publish": true 10 | }, 11 | 12 | "github": { 13 | "release": true 14 | }, 15 | 16 | "plugins": { 17 | "release-it-lerna-changelog": { 18 | "infile": "CHANGELOG.md" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 6 |

Starts enabled

7 |
8 |
9 | 10 |
11 | 16 |

Hi!

17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.2", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--welcome" 15 | ] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Closes # . 12 | 13 | ## Changes proposed in this pull request 14 | 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/my-class-raf.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | 5 | export default class MyRafClass extends Component { 6 | @service inViewport; 7 | 8 | @action 9 | setupViewport() { 10 | const loader = document.getElementById('loader'); 11 | const { onEnter } = this.inViewport.watchElement(loader); 12 | onEnter(this.didEnterViewport.bind(this)); 13 | } 14 | 15 | didEnterViewport() { 16 | this.args.infinityLoad(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-scrollable.hbs: -------------------------------------------------------------------------------- 1 |
2 |
    3 | {{#each this.model as |val|}} 4 |
    5 | 6 | {{{val}}} 7 | 8 |
    9 | {{/each}} 10 |
11 | 18 |
19 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Application from '../../app'; 2 | import config from '../../config/environment'; 3 | import { merge } from '@ember/polyfills'; 4 | import { run } from '@ember/runloop'; 5 | 6 | export default function startApp(attrs) { 7 | let attributes = merge({}, config.APP); 8 | attributes.autoboot = true; 9 | attributes = merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | return run(() => { 12 | let application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | return application; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-right-left.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | let rect = 4 | ''; 5 | let circle = 6 | ''; 7 | let line = 8 | ''; 9 | 10 | const images = [rect, circle, line]; 11 | 12 | export default Route.extend({ 13 | model() { 14 | const arr = Array.apply(null, Array(10)); 15 | return [...arr.map(() => `${images[(Math.random() * images.length) | 0]}`)]; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL, 7 | }); 8 | 9 | Router.map(function () { 10 | this.route('infinity'); 11 | this.route('infinity-custom-element'); 12 | this.route('infinity-modifier'); 13 | this.route('infinity-built-in-modifiers'); 14 | this.route('infinity-scrollable'); 15 | this.route('infinity-scrollable-raf'); 16 | this.route('infinity-scrollable-scrollevent'); 17 | this.route('infinity-right-left'); 18 | this.route('infinity-class'); 19 | }); 20 | 21 | export default Router; 22 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-scrollable-scrollevent.hbs: -------------------------------------------------------------------------------- 1 |
2 |
    3 | {{#each this.model as |val|}} 4 |
    5 | 6 | {{{val}}} 7 | 8 |
    9 | {{/each}} 10 |
11 | 19 |
20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.github/ 18 | /.gitignore 19 | /.prettierignore 20 | /.prettierrc.js 21 | /.release-it.json 22 | /.template-lintrc.js 23 | /.travis.yml 24 | /.watchmanconfig 25 | /bower.json 26 | /config/ember-try.js 27 | /CODE_OF_CONDUCT.md 28 | /CONTRIBUTING.md 29 | /ember-cli-build.js 30 | /testem.js 31 | /tests/ 32 | /yarn-error.log 33 | /yarn.lock 34 | .gitkeep 35 | 36 | # ember-try 37 | /.node_modules.ember-try/ 38 | /bower.json.ember-try 39 | /package.json.ember-try 40 | -------------------------------------------------------------------------------- /tests/dummy/public/assets/missing-artwork.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | const { maybeEmbroider } = require('@embroider/test-setup'); 18 | return maybeEmbroider(app, { 19 | skipBabel: [ 20 | { 21 | package: 'qunit', 22 | }, 23 | ], 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /tests/dummy/app/components/my-class.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | 5 | export default class MyClass extends Component { 6 | @service inViewport; 7 | 8 | @action 9 | setupViewport() { 10 | const loader = document.getElementById('loader'); 11 | const { onEnter } = this.inViewport.watchElement(loader); 12 | onEnter(this.didEnterViewport.bind(this)); 13 | } 14 | 15 | didEnterViewport() { 16 | this.infinityLoad(); 17 | } 18 | 19 | willDestroy() { 20 | super.willDestroy(...arguments); 21 | 22 | const loader = document.getElementById('loader'); 23 | this.inViewport.stopWatching(loader); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-right-left.hbs: -------------------------------------------------------------------------------- 1 |
2 |
    3 | {{#each this.model as |val|}} 4 |
    5 | 6 | {{{val}}} 7 | 8 |
    9 | {{/each}} 10 | 11 | 19 |   20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/unit/initializers/viewport-config-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | const { keys } = Object; 5 | 6 | module('Unit | Initializer | viewport config', function (hooks) { 7 | setupTest(hooks); 8 | 9 | test('it has a viewportConfig object', function (assert) { 10 | const viewportConfig = this.owner.lookup('config:in-viewport'); 11 | const viewportConfigKeys = keys(viewportConfig); 12 | 13 | assert.ok(viewportConfig); 14 | assert.ok(viewportConfigKeys.length); 15 | assert.ok( 16 | viewportConfigKeys.includes('intersectionThreshold'), 17 | 'intersectionThreshold is in viewportConfig' 18 | ); 19 | assert.ok( 20 | viewportConfigKeys.includes('scrollableArea'), 21 | 'scrollableArea is in viewportConfig' 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-class.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 | {{#each this.models as |val|}} 5 |
    6 | 7 | {{{val}}} 8 | 9 |
    10 | {{/each}} 11 |
12 | 13 | 14 |
15 | 16 |
17 |
    18 | {{#each this.models as |val|}} 19 |
    20 | 21 | {{{val}}} 22 | 23 |
    24 | {{/each}} 25 |
26 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /addon/utils/is-in-viewport.js: -------------------------------------------------------------------------------- 1 | const defaultTolerance = { 2 | top: 0, 3 | left: 0, 4 | bottom: 0, 5 | right: 0, 6 | }; 7 | 8 | export default function isInViewport( 9 | boundingClientRect = {}, 10 | height = 0, 11 | width = 0, 12 | tolerance = defaultTolerance 13 | ) { 14 | const { top, left, bottom, right, height: h, width: w } = boundingClientRect; 15 | const tolerances = Object.assign( 16 | Object.assign({}, defaultTolerance), 17 | tolerance 18 | ); 19 | const { 20 | top: topTolerance, 21 | left: leftTolerance, 22 | bottom: bottomTolerance, 23 | right: rightTolerance, 24 | } = tolerances; 25 | 26 | return ( 27 | top + topTolerance >= 0 && 28 | left + leftTolerance >= 0 && 29 | Math.round(bottom) - bottomTolerance - h <= Math.round(height) && 30 | Math.round(right) - rightTolerance - w <= Math.round(width) 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /addon/utils/check-scroll-direction.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | const { floor } = Math; 3 | 4 | export default function checkScrollDirection( 5 | lastPosition = null, 6 | newPosition = {}, 7 | sensitivity = 1 8 | ) { 9 | if (!lastPosition) { 10 | return 'none'; 11 | } 12 | 13 | assert('sensitivity cannot be 0', sensitivity); 14 | 15 | const { top, left } = newPosition; 16 | const { top: lastTop, left: lastLeft } = lastPosition; 17 | 18 | const delta = { 19 | top: floor((top - lastTop) / sensitivity) * sensitivity, 20 | left: floor((left - lastLeft) / sensitivity) * sensitivity, 21 | }; 22 | 23 | if (delta.top > 0) { 24 | return 'down'; 25 | } 26 | 27 | if (delta.top < 0) { 28 | return 'up'; 29 | } 30 | 31 | if (delta.left > 0) { 32 | return 'right'; 33 | } 34 | 35 | if (delta.left < 0) { 36 | return 'left'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/infinity-scrollable.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { later } from '@ember/runloop'; 3 | import { Promise } from 'rsvp'; 4 | 5 | let rect = 6 | ''; 7 | let circle = 8 | ''; 9 | let line = 10 | ''; 11 | 12 | const images = [rect, circle, line]; 13 | 14 | export default Route.extend({ 15 | model() { 16 | const arr = Array.apply(null, Array(10)); 17 | let models = [ 18 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 19 | ]; 20 | return new Promise((resolve) => { 21 | later(() => { 22 | resolve(models); 23 | }, 0); 24 | }); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

ember-in-viewport

2 | 3 |
    4 |
  • Infinity IntersectionObserver
  • 5 |
  • Infinity with a Modifier
  • 6 |
  • Infinity with built-in Modifiers
  • 7 |
  • Infinity with a Class / Class rAF
  • 8 |
  • Infinity Scrollable IntersectionObserver
  • 9 |
  • Infinity Scrollable rAF
  • 10 |
  • Infinity Scrollable Event
  • 11 |
  • Infinity with a Custom Element
  • 12 |
  • Infinity Right Left
  • 13 |
14 | 15 | {{outlet}} 16 | -------------------------------------------------------------------------------- /tests/dummy/app/components/my-modifier.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | 5 | export default class MyModifier extends Component { 6 | @service inViewport; 7 | 8 | @action 9 | setupInViewport(element) { 10 | const viewportSpy = true; 11 | const viewportTolerance = { 12 | bottom: 300, 13 | }; 14 | const { onEnter } = this.inViewport.watchElement(element, { 15 | viewportSpy, 16 | viewportTolerance, 17 | }); 18 | onEnter(this.didEnterViewport.bind(this)); 19 | } 20 | 21 | constructor() { 22 | super(...arguments); 23 | } 24 | 25 | didEnterViewport() { 26 | this.args.infinityLoad(); 27 | } 28 | 29 | willDestroy() { 30 | super.willDestroy(...arguments); 31 | 32 | const loader = document.getElementById('loader'); 33 | this.inViewport.stopWatching(loader); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-right-left.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | 4 | let rect = 5 | ''; 6 | let circle = 7 | ''; 8 | let line = 9 | ''; 10 | 11 | const images = [rect, circle, line]; 12 | 13 | export default class InfinityRightLeft extends Controller { 14 | viewportToleranceOverride = { 15 | right: 100, 16 | }; 17 | 18 | @action 19 | infinityLoad() { 20 | const arr = Array.apply(null, Array(10)); 21 | const newModels = [ 22 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 23 | ]; 24 | const models = this.model; 25 | models.push(...newModels); 26 | this.model = Array.prototype.slice.call(models); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-scrollable.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { set, action } from '@ember/object'; 3 | 4 | let rect = 5 | ''; 6 | let circle = 7 | ''; 8 | let line = 9 | ''; 10 | 11 | const images = [rect, circle, line]; 12 | 13 | export default class InfinityScrollable extends Controller { 14 | viewportToleranceOverride = { 15 | top: 1, 16 | }; 17 | 18 | @action 19 | infinityLoad() { 20 | const arr = Array.apply(null, Array(10)); 21 | const newModels = [ 22 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 23 | ]; 24 | const models = this.model; 25 | models.push(...newModels); 26 | set(this, 'model', Array.prototype.slice.call(models)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | // BASE 2 | a.active { 3 | color: magenta; 4 | } 5 | 6 | .my-component { 7 | display: inline-block; 8 | width: 300px; 9 | background-color: black; 10 | } 11 | 12 | .my-component p { 13 | color: white; 14 | } 15 | 16 | .my-component.active { 17 | background-color: red; 18 | } 19 | 20 | .my-component.top { 21 | margin-bottom: 300px; 22 | } 23 | 24 | .hide-me { 25 | margin-top: 1500px; 26 | } 27 | 28 | #ember-testing-container { 29 | margin-bottom: 500px !important; 30 | top: 0; 31 | } 32 | 33 | .infinity-container { 34 | border: 1px solid black; 35 | height: 500px; 36 | width: 70%; 37 | overflow: scroll; 38 | } 39 | .infinity-component, .infinity-scrollable { 40 | height: 10px; 41 | background-color: red; 42 | } 43 | 44 | .infinity-scrollable { 45 | width: 100px; 46 | min-height: 1px; 47 | } 48 | 49 | .class-components { 50 | display: flex; 51 | justify-content: space-between; 52 | } 53 | 54 | #raf-loader { 55 | height: 10px; 56 | } 57 | -------------------------------------------------------------------------------- /tests/dummy/app/-config/index.js: -------------------------------------------------------------------------------- 1 | import ENV from 'dummy/config/environment'; 2 | 3 | export const artworkProfiles = { 4 | dummy: { 5 | jumbo: { width: 300, height: 300 }, 6 | desktop: { width: 300, height: 300 }, 7 | tablet: { width: 200, height: 200 }, 8 | mobile: { width: 100, height: 100 }, 9 | }, 10 | }; 11 | 12 | export const artworkFallbacks = { 13 | 'missing-artwork': { 14 | isVector: true, 15 | url: `${ENV.rootURL}assets/missing-artwork.svg`, 16 | aspectRatio: 1, 17 | }, 18 | }; 19 | 20 | export const viewports = [ 21 | { 22 | mediaQuery: null, 23 | mediaQueryStrict: '(max-width:500px)', 24 | name: 'mobile', 25 | }, 26 | { 27 | mediaQuery: '(min-width:750px)', 28 | mediaQueryStrict: '(min-width:1000px) and (max-width:1319px)', 29 | name: 'tablet', 30 | }, 31 | { 32 | mediaQuery: '(min-width:1000px)', 33 | mediaQueryStrict: '', 34 | name: 'desktop', 35 | }, 36 | { 37 | mediaQuery: '(min-width:1320px)', 38 | mediaQueryStrict: '', 39 | name: 'jumbo', 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /addon/utils/can-use-raf.js: -------------------------------------------------------------------------------- 1 | // Adapted from Paul Irish's rAF polyfill 2 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 3 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 4 | 5 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 6 | 7 | // MIT license 8 | 9 | import canUseDOM from 'ember-in-viewport/utils/can-use-dom'; 10 | 11 | function checkRAF(window, rAF, cAF) { 12 | let x; 13 | let vendors = ['ms', 'moz', 'webkit', 'o']; 14 | 15 | for (x = 0; x < vendors.length && !window[rAF]; ++x) { 16 | window[rAF] = window[`${vendors[x]}RequestAnimationFrame`]; 17 | window[cAF] = 18 | window[`${vendors[x]}CancelAnimationFrame`] || 19 | window[`${vendors[x]}CancelRequestAnimationFrame`]; 20 | } 21 | 22 | if (window[rAF] && window[cAF]) { 23 | return true; 24 | } else { 25 | return false; 26 | } 27 | } 28 | 29 | export default function canUseRAF() { 30 | if (!canUseDOM) { 31 | return false; 32 | } 33 | 34 | return checkRAF(window, 'requestAnimationFrame', 'cancelAnimationFrame'); 35 | } 36 | -------------------------------------------------------------------------------- /addon/initializers/viewport-config.js: -------------------------------------------------------------------------------- 1 | import canUseDOM from 'ember-in-viewport/utils/can-use-dom'; 2 | 3 | const defaultConfig = { 4 | viewportDidScroll: true, 5 | viewportSpy: false, 6 | viewportListeners: [ 7 | { context: window, event: 'scroll' }, 8 | { context: window, event: 'resize' }, 9 | ], 10 | viewportTolerance: { 11 | top: 0, 12 | left: 0, 13 | bottom: 0, 14 | right: 0, 15 | }, 16 | intersectionThreshold: 0, 17 | scrollableArea: null, // defaults to layout view (document.documentElement) 18 | }; 19 | 20 | if (canUseDOM) { 21 | defaultConfig.viewportListeners.push({ 22 | context: document, 23 | event: 'touchmove', 24 | }); 25 | } 26 | 27 | export function initialize() { 28 | const application = arguments[1] || arguments[0]; 29 | const config = application.resolveRegistration('config:environment'); 30 | const { viewportConfig = {} } = config; 31 | const mergedConfig = Object.assign({}, defaultConfig, viewportConfig); 32 | 33 | application.register('config:in-viewport', mergedConfig, { 34 | instantiate: false, 35 | }); 36 | } 37 | 38 | export default { 39 | name: 'viewport-config', 40 | initialize: initialize, 41 | }; 42 | -------------------------------------------------------------------------------- /addon/utils/can-use-intersection-observer.js: -------------------------------------------------------------------------------- 1 | // Adapted from WC3's intersection polyfill 2 | // https://github.com/w3c/IntersectionObserver/blob/master/polyfill/intersection-observer.js 3 | 4 | import canUseDOM from 'ember-in-viewport/utils/can-use-dom'; 5 | 6 | function checkIntersectionObserver(window) { 7 | if ( 8 | 'IntersectionObserver' in window && 9 | 'IntersectionObserverEntry' in window && 10 | 'intersectionRatio' in window.IntersectionObserverEntry.prototype 11 | ) { 12 | // Minimal polyfill for Edge 15's lack of `isIntersecting` 13 | // See: https://github.com/w3c/IntersectionObserver/issues/211 14 | if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) { 15 | Object.defineProperty( 16 | window.IntersectionObserverEntry.prototype, 17 | 'isIntersecting', 18 | { 19 | get: function () { 20 | return this.intersectionRatio > 0; 21 | }, 22 | } 23 | ); 24 | } 25 | return true; 26 | } 27 | return false; 28 | } 29 | 30 | export default function canUseIntersectionObserver() { 31 | if (!canUseDOM) { 32 | return false; 33 | } 34 | 35 | return checkIntersectionObserver(window); 36 | } 37 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-scrollable-raf.js: -------------------------------------------------------------------------------- 1 | import InfinityScrollable from './infinity-scrollable'; 2 | import { set, action } from '@ember/object'; 3 | 4 | let rect = 5 | ''; 6 | let circle = 7 | ''; 8 | let line = 9 | ''; 10 | 11 | const images = [rect, circle, line]; 12 | const arr = Array.apply(null, Array(10)); 13 | const otherModels = [ 14 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 15 | ]; 16 | 17 | export default class InfinityScrollableRaf extends InfinityScrollable { 18 | viewportToleranceOverride = { 19 | bottom: 200, 20 | }; 21 | 22 | otherModels = otherModels; 23 | 24 | @action 25 | infinityLoadOther() { 26 | const arr = Array.apply(null, Array(10)); 27 | const newModels = [ 28 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 29 | ]; 30 | const models = this.otherModels; 31 | models.push(...newModels); 32 | set(this, 'otherModels', Array.prototype.slice.call(models)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-class.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action, set } from '@ember/object'; 3 | 4 | let rect = 5 | ''; 6 | let circle = 7 | ''; 8 | let line = 9 | ''; 10 | 11 | const images = [rect, circle, line]; 12 | const arr = Array.apply(null, Array(10)); 13 | const models = [ 14 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 15 | ]; 16 | 17 | export default class InfinityClass extends Controller { 18 | constructor() { 19 | super(...arguments); 20 | this.viewportToleranceOverride = { 21 | bottom: 200, 22 | }; 23 | } 24 | 25 | models = models; 26 | 27 | @action 28 | infinityLoad() { 29 | const arr = Array.apply(null, Array(10)); 30 | const newModels = [ 31 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 32 | ]; 33 | const models = this.models; 34 | models.push(...newModels); 35 | set(this, 'models', Array.prototype.slice.call(models)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-modifier.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action, set } from '@ember/object'; 3 | 4 | let rect = 5 | ''; 6 | let circle = 7 | ''; 8 | let line = 9 | ''; 10 | 11 | const images = [rect, circle, line]; 12 | const arr = Array.apply(null, Array(10)); 13 | const models = [ 14 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 15 | ]; 16 | 17 | export default class InfinityModifier extends Controller { 18 | constructor() { 19 | super(...arguments); 20 | this.viewportToleranceOverride = { 21 | bottom: 200, 22 | }; 23 | } 24 | 25 | models = models; 26 | 27 | @action 28 | infinityLoad() { 29 | const arr = Array.apply(null, Array(10)); 30 | const newModels = [ 31 | ...arr.map(() => `${images[(Math.random() * images.length) | 0]}`), 32 | ]; 33 | const models = this.models; 34 | models.push(...newModels); 35 | set(this, 'models', Array.prototype.slice.call(models)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/services/-observer-admin-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | import ObserverAdmin from 'ember-in-viewport/-private/observer-admin'; 4 | 5 | module('Unit Class | -observer-admin', function (hooks) { 6 | setupTest(hooks); 7 | 8 | // https://github.com/DockYard/ember-in-viewport/issues/160 9 | test('handles root element gaining custom properties', function (assert) { 10 | let service = new ObserverAdmin(); 11 | let root = document.createElement('div'); 12 | let observerOptions = { 13 | root, 14 | rootMargin: '0px 0px 100px 0px', 15 | threshold: 0, 16 | }; 17 | 18 | service.add(root, observerOptions); 19 | assert.ok(true); 20 | }); 21 | 22 | test('handles root element gaining custom properties with scrollableArea', function (assert) { 23 | let service = new ObserverAdmin(); 24 | let root = document.createElement('div'); 25 | let observerOptions = { 26 | root, 27 | rootMargin: '0px 0px 100px 0px', 28 | threshold: 0, 29 | scrollableArea: '.main-area', 30 | }; 31 | 32 | service.add( 33 | root, 34 | observerOptions, 35 | () => {}, 36 | () => {} 37 | ); 38 | assert.ok(true); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-scrollable-raf.hbs: -------------------------------------------------------------------------------- 1 |
2 |
    3 | {{#each this.model as |val|}} 4 |
    5 | 6 | {{{val}}} 7 | 8 |
    9 | {{/each}} 10 |
11 | 19 |
20 | 21 |
22 | 23 |
    24 | {{#each this.otherModels as |val|}} 25 |
    26 | 27 | {{{val}}} 28 | 29 |
    30 | {{/each}} 31 |
32 | 38 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { set, action } from '@ember/object'; 3 | import { later } from '@ember/runloop'; 4 | import { Promise } from 'rsvp'; 5 | 6 | const images = ['jarjan', 'aio___', 'kushsolitary', 'kolage', 'idiot', 'gt']; 7 | 8 | const arr = Array.apply(null, Array(10)); 9 | const models = [ 10 | ...arr.map( 11 | () => 12 | `https://s3.amazonaws.com/uifaces/faces/twitter/${ 13 | images[(Math.random() * images.length) | 0] 14 | }/128.jpg` 15 | ), 16 | ]; 17 | 18 | export default class Infinity extends Controller { 19 | models = models; 20 | 21 | viewportToleranceOverride = { 22 | bottom: 300, 23 | }; 24 | 25 | @action 26 | infinityLoad() { 27 | const arr = Array.apply(null, Array(10)); 28 | const newModels = [ 29 | ...arr.map( 30 | () => 31 | `https://s3.amazonaws.com/uifaces/faces/twitter/${ 32 | images[(Math.random() * images.length) | 0] 33 | }/128.jpg` 34 | ), 35 | ]; 36 | return new Promise((resolve) => { 37 | later(() => { 38 | const models = this.models; 39 | models.push(...newModels); 40 | set(this, 'models', Array.prototype.slice.call(models)); 41 | resolve(); 42 | }, 0); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | ## Version 23 | 24 | 25 | ## Test Case 26 | 27 | 28 | ## Steps to reproduce 29 | 30 | 31 | ## Expected Behavior 32 | 33 | 34 | ## Actual Behavior 35 | 36 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: { 23 | 'getter-return': 0, 24 | 'ember/no-classic-classes': 0, 25 | 'ember/no-classic-components': 0, 26 | 'ember/require-tagless-components': 0, 27 | }, 28 | overrides: [ 29 | // node files 30 | { 31 | files: [ 32 | './.eslintrc.js', 33 | './.prettierrc.js', 34 | './.template-lintrc.js', 35 | './ember-cli-build.js', 36 | './index.js', 37 | './testem.js', 38 | './blueprints/*/index.js', 39 | './config/**/*.js', 40 | './tests/dummy/config/**/*.js', 41 | ], 42 | parserOptions: { 43 | sourceType: 'script', 44 | }, 45 | env: { 46 | browser: false, 47 | node: true, 48 | }, 49 | plugins: ['node'], 50 | extends: ['plugin:node/recommended'], 51 | }, 52 | { 53 | // Test files: 54 | files: ['tests/**/*-test.{js,ts}'], 55 | extends: ['plugin:qunit/recommended'], 56 | }, 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /tests/integration/components/my-component-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import { hbs } from 'ember-cli-htmlbars'; 5 | 6 | module('Integration | Component | my component', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders with viewportTolerance partially set', async function (assert) { 10 | // viewportTolerance is usually setup in the initializer. Needs defaults 11 | this.viewportToleranceOverride = { 12 | top: 1, 13 | }; 14 | await render(hbs` 15 | 16 | template block text 17 | 18 | `); 19 | 20 | assert.equal(this.element.textContent.trim(), 'template block text'); 21 | }); 22 | 23 | test('it renders with intersectionThreshold set', async function (assert) { 24 | this.viewportTolerance = { 25 | top: 0, 26 | left: 0, 27 | right: 0, 28 | bottom: 0, 29 | }; 30 | this.intersectionThreshold = 1.0; 31 | 32 | await render(hbs` 33 | 34 | template block text 35 | 36 | `); 37 | 38 | assert.equal(this.element.textContent.trim(), 'template block text'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/unit/raf-pool-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | import RafPool from 'raf-pool'; 4 | 5 | /** 6 | * Tests for raf-pool dependency 7 | * 8 | * @see {@link https://www.npmjs.com/package/raf-pool} 9 | */ 10 | 11 | module('Unit | Raf Pool', function (hooks) { 12 | setupTest(hooks); 13 | 14 | hooks.beforeEach(function () { 15 | this.rAFPoolManager = new RafPool(); 16 | }); 17 | 18 | test('can add to pool manager', function (assert) { 19 | let cb = () => {}; 20 | let obj = this.rAFPoolManager.add(123, cb); 21 | assert.equal(obj, cb); 22 | }); 23 | 24 | test('can remove from pool manager (single element)', function (assert) { 25 | let cb = () => {}; 26 | this.rAFPoolManager.add(123, cb); 27 | assert.equal(this.rAFPoolManager.pool.length, 1); 28 | this.rAFPoolManager.remove(123); 29 | assert.equal(this.rAFPoolManager.pool.length, 0); 30 | }); 31 | 32 | test('can remove from pool manager (multiple elements)', function (assert) { 33 | let cb = () => {}; 34 | this.rAFPoolManager.add(123, cb); 35 | this.rAFPoolManager.add(124, cb); 36 | assert.equal(this.rAFPoolManager.pool.length, 2); 37 | this.rAFPoolManager.remove(123); 38 | assert.equal(this.rAFPoolManager.pool.length, 1); 39 | assert.equal(this.rAFPoolManager.pool[0]['124'], cb); 40 | }); 41 | 42 | test('reset resets pool manager', function (assert) { 43 | let cb = () => {}; 44 | this.rAFPoolManager.add(123, cb); 45 | this.rAFPoolManager.add(124, cb); 46 | assert.equal(this.rAFPoolManager.pool.length, 2); 47 | this.rAFPoolManager.reset(); 48 | assert.equal(this.rAFPoolManager.pool.length, 0); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/unit/utils/check-scroll-direction-test.js: -------------------------------------------------------------------------------- 1 | import checkScrollDirection from 'ember-in-viewport/utils/check-scroll-direction'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | let lastPosition; 6 | 7 | module('Unit | Utility | check scroll direction', function (hooks) { 8 | setupTest(hooks); 9 | 10 | hooks.beforeEach(function () { 11 | lastPosition = { 12 | top: 300, 13 | left: 150, 14 | }; 15 | }); 16 | 17 | test('returns the right direction', function (assert) { 18 | const movements = [ 19 | { direction: 'down', position: { top: 400, left: 150 } }, 20 | { direction: 'up', position: { top: 200, left: 150 } }, 21 | { direction: 'right', position: { top: 300, left: 250 } }, 22 | { direction: 'left', position: { top: 300, left: 100 } }, 23 | ]; 24 | 25 | assert.expect(movements.length); 26 | 27 | movements.forEach((movement) => { 28 | const { direction, position } = movement; 29 | const scrollDirection = checkScrollDirection(lastPosition, position); 30 | 31 | assert.equal(direction, scrollDirection); 32 | }); 33 | }); 34 | 35 | test('adjusts for sensitivity', function (assert) { 36 | const movements = [ 37 | { direction: undefined, position: { top: 399, left: 150 } }, 38 | { direction: 'down', position: { top: 400, left: 150 } }, 39 | { direction: 'down', position: { top: 500, left: 250 } }, 40 | ]; 41 | 42 | assert.expect(movements.length); 43 | 44 | movements.forEach((movement) => { 45 | const { direction, position } = movement; 46 | const scrollDirection = checkScrollDirection(lastPosition, position, 100); 47 | 48 | assert.equal(direction, scrollDirection); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/dummy-artwork.hbs: -------------------------------------------------------------------------------- 1 |
8 | 9 | {{#if this.actualArtwork}} 10 | {{#if this.isUserMonogram}} 11 | 12 | {{this.userInitials}} 13 | 14 | {{else if this.isFallbackArtwork}} 15 | {{@alt}} 21 | 22 | {{else if this.lazyLoad}} 23 | {{@alt}} 32 | 33 | {{else}} 34 | {{@alt}} 43 | 44 | {{/if}} 45 | {{/if}} 46 | 47 | {{yield}} 48 |
49 | -------------------------------------------------------------------------------- /tests/acceptance/integration-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupApplicationTest } from 'ember-qunit'; 3 | import { find, visit, waitFor } from '@ember/test-helpers'; 4 | 5 | module('Acceptance | Intersection Observer', function (hooks) { 6 | setupApplicationTest(hooks); 7 | 8 | hooks.beforeEach(function () { 9 | // bring testem window up to the top. 10 | document.getElementById('ember-testing-container').scrollTop = 0; 11 | }); 12 | 13 | test('Component is active when in viewport', async function (assert) { 14 | assert.expect(1); 15 | 16 | await visit('/'); 17 | 18 | await waitFor('.my-component.top.start-enabled.active'); 19 | assert.ok( 20 | find('.my-component.top.start-enabled.active'), 21 | 'component is active' 22 | ); 23 | }); 24 | 25 | test('Component is inactive when not in viewport', async function (assert) { 26 | assert.expect(1); 27 | 28 | await visit('/'); 29 | 30 | assert.ok(find('.my-component.bottom.inactive'), 'component is inactive'); 31 | }); 32 | 33 | test('Component moves to active when scrolled into viewport', async function (assert) { 34 | assert.expect(2); 35 | 36 | await visit('/'); 37 | 38 | assert.ok(find('.my-component.bottom.inactive'), 'component is inactive'); 39 | document.querySelector('.my-component.bottom').scrollIntoView(); 40 | 41 | await waitFor('.my-component.bottom.active'); 42 | assert.ok(find('.my-component.bottom.active'), 'component is active'); 43 | }); 44 | 45 | test('Component moves back to inactive when scrolled out of viewport', async function (assert) { 46 | assert.expect(1); 47 | 48 | await visit('/'); 49 | 50 | document.querySelector('.my-component.bottom').scrollIntoView(false); 51 | 52 | await waitFor('.my-component.top.start-enabled.inactive'); 53 | assert.ok( 54 | find('.my-component.top.start-enabled.inactive'), 55 | 'component is inactive' 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/infinity-built-in-modifiers.hbs: -------------------------------------------------------------------------------- 1 | 7 |
8 |

9 | 10 | {{#if (eq this.direction "both")}} 11 |
    12 | {{#each this.models as |artwork i|}} 13 |
  • 16 | 17 |
  • 18 | {{/each}} 19 |
    29 |
30 | {{else if (eq this.direction "enter")}} 31 |
    32 | {{#each this.models as |artwork|}} 33 |
  • 34 | 35 |
  • 36 | {{/each}} 37 |
    38 |
39 | {{else if (eq this.direction "exit")}} 40 |
    41 | {{#each this.models as |artwork|}} 42 |
  • 43 | 44 |
  • 45 | {{/each}} 46 |
    47 |
48 | {{/if}} 49 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | jobs: 11 | test: 12 | name: "Tests" 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 12.x 21 | cache: npm 22 | - name: Install Dependencies 23 | run: npm ci 24 | - name: Lint 25 | run: npm run lint 26 | - name: Run Tests 27 | run: npm run test:ember 28 | 29 | floating: 30 | name: "Floating Dependencies" 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v2 36 | with: 37 | node-version: 12.x 38 | cache: npm 39 | - name: Install Dependencies 40 | run: npm install --no-shrinkwrap 41 | - name: Run Tests 42 | run: npm run test:ember 43 | 44 | try-scenarios: 45 | name: ${{ matrix.try-scenario }} 46 | runs-on: ubuntu-latest 47 | needs: 'test' 48 | 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | try-scenario: 53 | - ember-lts-3.16 54 | - ember-lts-3.20 55 | - ember-lts-3.24 56 | - ember-lts-3.28 57 | - ember-release 58 | - ember-beta 59 | - ember-canary 60 | - ember-classic 61 | - embroider-safe 62 | - embroider-optimized 63 | - ember-modifier@2 64 | - ember-modifier@3.1 65 | - ember-modifier@3.2 66 | - ember-modifier@^4.0.0-beta.1 67 | 68 | steps: 69 | - uses: actions/checkout@v2 70 | - name: Install Node 71 | uses: actions/setup-node@v2 72 | with: 73 | node-version: 12.x 74 | cache: npm 75 | - name: Install Dependencies 76 | run: npm ci 77 | - name: Run Tests 78 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} 79 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-built-in-modifiers.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { later } from '@ember/runloop'; 3 | import { action, set } from '@ember/object'; 4 | 5 | const images = ['jarjan', 'aio___', 'kushsolitary', 'kolage', 'idiot', 'gt']; 6 | 7 | export default class InfinityBuiltInModifiers extends Controller { 8 | queryParams = ['direction']; 9 | direction = 'both'; 10 | 11 | constructor() { 12 | super(...arguments); 13 | 14 | this.models = [ 15 | ...Array.apply(null, Array(10)).map(() => { 16 | return { 17 | bgColor: 'E8D26F', 18 | url: `https://s3.amazonaws.com/uifaces/faces/twitter/${ 19 | images[(Math.random() * images.length) | 0] 20 | }/128.jpg`, 21 | }; 22 | }), 23 | ]; 24 | set(this, 'viewportTolerance', { 25 | bottom: 300, 26 | }); 27 | } 28 | 29 | @action 30 | setTitle(element) { 31 | element.textContent = '{{in-viewport}} modifier'; 32 | } 33 | 34 | @action 35 | setTitleGreen() { 36 | document.querySelector('h1#green-target').style = 'color: green'; 37 | } 38 | 39 | @action 40 | removeTitleGreen() { 41 | document.querySelector('h1#green-target').style = ''; 42 | } 43 | 44 | @action 45 | didEnterViewport(/*artwork, i, element*/) { 46 | const arr = Array.apply(null, Array(10)); 47 | const newModels = [ 48 | ...arr.map(() => { 49 | return { 50 | bgColor: '0790EB', 51 | url: `https://s3.amazonaws.com/uifaces/faces/twitter/${ 52 | images[(Math.random() * images.length) | 0] 53 | }/128.jpg`, 54 | }; 55 | }), 56 | ]; 57 | 58 | return new Promise((resolve) => { 59 | later(() => { 60 | const models = this.models; 61 | models.push(...newModels); 62 | set(this, 'models', Array.prototype.slice.call(models)); 63 | resolve(); 64 | }, 0); 65 | }); 66 | } 67 | 68 | @action 69 | didExitViewport(/*artwork, i, element*/) { 70 | // console.log('exit', { artwork, i, element }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/infinity-custom-element.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action, set } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | 5 | class CustomSentinel extends HTMLElement { 6 | constructor() { 7 | super(); 8 | this.attachShadow({ mode: 'open' }); 9 | } 10 | 11 | connectedCallback() { 12 | this.shadowRoot.innerHTML = `
`; 13 | } 14 | } 15 | 16 | window.customElements.define('custom-sentinel', CustomSentinel); 17 | 18 | const images = ['jarjan', 'aio___', 'kushsolitary', 'kolage', 'idiot', 'gt']; 19 | 20 | const arr = Array.apply(null, Array(10)); 21 | const models = [ 22 | ...arr.map(() => { 23 | return { 24 | bgColor: 'E8D26F', 25 | url: `https://s3.amazonaws.com/uifaces/faces/twitter/${ 26 | images[(Math.random() * images.length) | 0] 27 | }/128.jpg`, 28 | }; 29 | }), 30 | ]; 31 | 32 | export default class InfinityCustomElement extends Controller { 33 | @service inViewport; 34 | 35 | models = models; 36 | 37 | @action 38 | setupInViewport(element) { 39 | // find distance of top left corner of artwork to bottom of screen. Shave off 50px so user has to scroll slightly to trigger load 40 | window.requestAnimationFrame(() => { 41 | const { onEnter } = this.inViewport.watchElement(element, { 42 | viewportTolerance: { top: 200, right: 200, bottom: 200, left: 200 }, 43 | }); 44 | 45 | onEnter(this.infinityLoad.bind(this)); 46 | }); 47 | } 48 | 49 | @action 50 | infinityLoad() { 51 | const arr = Array.apply(null, Array(10)); 52 | const newModels = [ 53 | ...arr.map(() => { 54 | return { 55 | bgColor: '0790EB', 56 | url: `https://s3.amazonaws.com/uifaces/faces/twitter/${ 57 | images[(Math.random() * images.length) | 0] 58 | }/128.jpg`, 59 | }; 60 | }), 61 | ]; 62 | 63 | const models = this.models; 64 | models.push(...newModels); 65 | set(this, 'models', Array.prototype.slice.call(models)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/integration/components/modifier-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, scrollTo } from '@ember/test-helpers'; 4 | import { hbs } from 'ember-cli-htmlbars'; 5 | 6 | module('Integration | Modifiers | {{in-viewport}}', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it invokes the given `onEnter` and `onExist` callbacks passing the element and the intersectionObserverEntry', async function (assert) { 10 | assert.expect(4); 11 | this.onEnter = function (el, intersectionObserverEntry) { 12 | assert.dom(el).hasText('Four'); 13 | assert.ok( 14 | Math.abs(intersectionObserverEntry.intersectionRatio - 1) < 0.05, 15 | 'It receives an intersectionObserverEntry' 16 | ); 17 | }; 18 | this.onExit = function (el, intersectionObserverEntry) { 19 | assert.dom(el).hasText('One'); 20 | assert.ok( 21 | Math.abs(intersectionObserverEntry.intersectionRatio - 0.411) < 0.05, 22 | 'It receives an intersectionObserverEntry' 23 | ); 24 | }; 25 | await render(hbs` 26 |
27 |
One
34 |
Two
35 |
Three
36 |
Four
43 |
44 | `); 45 | await scrollTo('#wrapper', 0, 60); 46 | await new Promise((r) => setTimeout(r, 150)); 47 | await scrollTo('#wrapper', 0, 260); 48 | await new Promise((r) => setTimeout(r, 150)); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/dummy/app/components/my-component.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { action } from '@ember/object'; 4 | import { inject as service } from '@ember/service'; 5 | 6 | export default class MyComponent extends Component { 7 | @service inViewport; 8 | @tracked viewportEntered; 9 | 10 | // can't use preset id, because some tests use more than one instance on page 11 | elementRef; 12 | 13 | @action 14 | setupViewport(elementRef) { 15 | this.elementRef = elementRef; 16 | let options = {}; 17 | 18 | let { 19 | viewportSpyOverride, 20 | viewportIntersectionObserverOverride, 21 | viewportToleranceOverride, 22 | viewportRAFOverride, 23 | scrollableAreaOverride, 24 | intersectionThresholdOverride, 25 | } = this.args; 26 | 27 | if (viewportSpyOverride !== undefined) { 28 | options.viewportSpy = viewportSpyOverride; 29 | } 30 | if (viewportIntersectionObserverOverride !== undefined) { 31 | options.viewportUseIntersectionObserver = 32 | viewportIntersectionObserverOverride; 33 | } 34 | if (viewportToleranceOverride !== undefined) { 35 | options.viewportTolerance = viewportToleranceOverride; 36 | } 37 | if (viewportRAFOverride !== undefined) { 38 | options.viewportUseRAF = viewportRAFOverride; 39 | } 40 | if (scrollableAreaOverride !== undefined) { 41 | options.scrollableArea = scrollableAreaOverride; 42 | } 43 | if (intersectionThresholdOverride !== undefined) { 44 | options.intersectionThreshold = intersectionThresholdOverride; 45 | } 46 | 47 | const { onEnter, onExit } = this.inViewport.watchElement( 48 | elementRef, 49 | options 50 | ); 51 | onEnter(this.didEnterViewport.bind(this)); 52 | onExit(this.didExitViewport.bind(this)); 53 | } 54 | 55 | didEnterViewport() { 56 | this.viewportEntered = true; 57 | 58 | if (this.args.infinityLoad) { 59 | this.args.infinityLoad(); 60 | } 61 | } 62 | 63 | didExitViewport() { 64 | this.viewportEntered = false; 65 | } 66 | 67 | willDestroy() { 68 | super.willDestroy(...arguments); 69 | if (this.elementRef) this.inViewport.stopWatching(this.elementRef); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /addon/-private/observer-admin.js: -------------------------------------------------------------------------------- 1 | import IntersectionObserverAdmin from 'intersection-observer-admin'; 2 | 3 | /** 4 | * Static administrator to ensure use one IntersectionObserver per combination of root + observerOptions 5 | * Use `root` (viewport) as lookup property and weakly referenced 6 | * `root` will have many keys with each value being and object containing one IntersectionObserver instance and all the elements to observe 7 | * Provided callbacks will ensure consumer of this service is able to react to enter or exit of intersection observer 8 | * This provides important optimizations since we are not instantiating a new IntersectionObserver instance for every element and 9 | * instead reusing the instance. 10 | * 11 | * @class ObserverAdmin 12 | */ 13 | export default class ObserverAdmin { 14 | /** @private **/ 15 | constructor() { 16 | this.instance = new IntersectionObserverAdmin(); 17 | } 18 | 19 | /** 20 | * @method add 21 | * @param HTMLElement element 22 | * @param Object observerOptions 23 | * @param Function enterCallback 24 | * @param Function exitCallback 25 | * @void 26 | */ 27 | add(element, observerOptions, enterCallback, exitCallback) { 28 | if (enterCallback) { 29 | this.addEnterCallback(element, enterCallback); 30 | } 31 | if (exitCallback) { 32 | this.addExitCallback(element, exitCallback); 33 | } 34 | 35 | return this.instance.observe(element, observerOptions); 36 | } 37 | 38 | addEnterCallback(element, enterCallback) { 39 | this.instance.addEnterCallback(element, enterCallback); 40 | } 41 | 42 | addExitCallback(element, exitCallback) { 43 | this.instance.addExitCallback(element, exitCallback); 44 | } 45 | 46 | /** 47 | * This method takes a target element, observerOptions and a the scrollable area. 48 | * The latter two act as unique identifiers to figure out which intersection observer instance 49 | * needs to be used to call `unobserve` 50 | * 51 | * @method unobserve 52 | * @param HTMLElement target 53 | * @param Object observerOptions 54 | * @param String scrollableArea 55 | * @void 56 | */ 57 | unobserve(...args) { 58 | this.instance.unobserve(...args); 59 | } 60 | 61 | destroy(...args) { 62 | this.instance.destroy(...args); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Improve documentation 4 | 5 | We are always looking to improve our documentation. If at some moment you are 6 | reading the documentation and something is not clear, or you can't find what you 7 | are looking for, then please open an issue with the repository. This gives us a 8 | chance to answer your question and to improve the documentation if needed. 9 | 10 | Pull requests correcting spelling or grammar mistakes are always welcome. 11 | 12 | ## Found a bug? 13 | 14 | Please try to answer at least the following questions when reporting a bug: 15 | 16 | - Which version of the project did you use when you noticed the bug? 17 | - How do you reproduce the error condition? 18 | - What happened that you think is a bug? 19 | - What should it do instead? 20 | 21 | It would really help the maintainers if you could provide a reduced test case 22 | that reproduces the error condition. 23 | 24 | ## Have a feature request? 25 | 26 | Please provide some thoughful commentary and code samples on what this feature 27 | should do and why it should be added (your use case). The minimal questions you 28 | should answer when submitting a feature request should be: 29 | 30 | - What will it allow you to do that you can't do today? 31 | - Why do you need this feature and how will it benefit other users? 32 | - Are there any drawbacks to this feature? 33 | 34 | ## Submitting a pull-request? 35 | 36 | Here are some things that will increase the chance that your pull-request will 37 | get accepted: 38 | - Did you confirm this fix/feature is something that is needed? 39 | - Did you write tests, preferably in a test driven style? 40 | - Did you add documentation for the changes you made? 41 | - Did you follow our [styleguide](https://github.com/dockyard/styleguides)? 42 | 43 | If your pull-request addresses an issue then please add the corresponding 44 | issue's number to the description of your pull-request. 45 | 46 | # How to work with this project locally 47 | 48 | ## Installation 49 | 50 | First clone this repository: 51 | 52 | ```sh 53 | git clone https://github.com/DockYard/ember-in-viewport.git 54 | ``` 55 | 56 | 57 | 58 | ## Running tests 59 | 60 | 61 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | scenarios: [ 9 | { 10 | name: 'ember-lts-3.16', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': '~3.16.10', 14 | }, 15 | }, 16 | }, 17 | { 18 | name: 'ember-lts-3.20', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': '~3.20.5', 22 | }, 23 | }, 24 | }, 25 | { 26 | name: 'ember-lts-3.24', 27 | npm: { 28 | devDependencies: { 29 | 'ember-source': '~3.24.3', 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'ember-lts-3.28', 35 | npm: { 36 | devDependencies: { 37 | 'ember-source': '~3.28.4', 38 | }, 39 | }, 40 | }, 41 | { 42 | name: 'ember-release', 43 | npm: { 44 | devDependencies: { 45 | 'ember-source': await getChannelURL('release'), 46 | }, 47 | }, 48 | }, 49 | { 50 | name: 'ember-beta', 51 | npm: { 52 | devDependencies: { 53 | 'ember-source': await getChannelURL('beta'), 54 | }, 55 | }, 56 | }, 57 | { 58 | name: 'ember-canary', 59 | npm: { 60 | devDependencies: { 61 | 'ember-source': await getChannelURL('canary'), 62 | }, 63 | }, 64 | }, 65 | { 66 | name: 'ember-modifier@2', 67 | npm: { 68 | dependencies: { 69 | 'ember-modifier': '^2.1.2', 70 | }, 71 | }, 72 | }, 73 | { 74 | name: 'ember-modifier@3.1', 75 | npm: { 76 | dependencies: { 77 | 'ember-modifier': '^3.1.0', 78 | }, 79 | }, 80 | }, 81 | { 82 | name: 'ember-modifier@3.2', 83 | npm: { 84 | dependencies: { 85 | 'ember-modifier': '^3.2.7', 86 | }, 87 | }, 88 | }, 89 | { 90 | name: 'ember-modifier@^4.0.0-beta.1', 91 | npm: { 92 | dependencies: { 93 | 'ember-modifier': '^4.0.0-beta.1', 94 | }, 95 | }, 96 | }, 97 | { 98 | name: 'ember-classic', 99 | env: { 100 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 101 | 'application-template-wrapper': true, 102 | 'default-async-observers': false, 103 | 'template-only-glimmer-components': false, 104 | }), 105 | }, 106 | npm: { 107 | devDependencies: { 108 | 'ember-source': '~3.28.0', 109 | }, 110 | ember: { 111 | edition: 'classic', 112 | }, 113 | }, 114 | }, 115 | embroiderSafe(), 116 | embroiderOptimized(), 117 | ], 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at brian@dockyard.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-in-viewport", 3 | "version": "4.1.0", 4 | "description": "Detect if an Ember View or Component is in the viewport @ 60FPS", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "ember build --environment=production", 11 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 12 | "lint:hbs": "ember-template-lint .", 13 | "lint:js": "eslint .", 14 | "start": "ember serve", 15 | "test": "npm-run-all lint:* test:*", 16 | "test:ember": "ember test", 17 | "contributors": "npx contributor-faces -e \"(*-bot|*\\[bot\\]|*-tomster|homu|bors)\"", 18 | "release": "release-it" 19 | }, 20 | "repository": "https://github.com/dockyard/ember-in-viewport", 21 | "engines": { 22 | "node": "12.* || 14.* || >= 16" 23 | }, 24 | "author": "Scott Newcomer", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@embroider/macros": "^1.8.3", 28 | "ember-auto-import": "^2.2.3", 29 | "ember-cli-babel": "^7.26.6", 30 | "ember-destroyable-polyfill": "^2.0.3", 31 | "ember-modifier": "^2.1.2 || ^3.0.0 || ^4.0.0", 32 | "fast-deep-equal": "^2.0.1", 33 | "intersection-observer-admin": "~0.3.2", 34 | "raf-pool": "~0.1.4" 35 | }, 36 | "devDependencies": { 37 | "@ember/optional-features": "^2.0.0", 38 | "@ember/render-modifiers": "^2.0.0", 39 | "@ember/test-helpers": "^2.5.0", 40 | "@embroider/compat": "^1.8.3", 41 | "@embroider/core": "^1.8.3", 42 | "@embroider/test-setup": "^1.8.3", 43 | "@embroider/webpack": "^1.8.3", 44 | "@glimmer/component": "^1.0.4", 45 | "@glimmer/tracking": "^1.0.4", 46 | "babel-eslint": "^10.1.0", 47 | "broccoli-asset-rev": "^3.0.0", 48 | "ember-cli": "~3.28.2", 49 | "ember-cli-dependency-checker": "^3.2.0", 50 | "ember-cli-htmlbars": "^5.7.1", 51 | "ember-cli-inject-live-reload": "^2.1.0", 52 | "ember-cli-sri": "^2.1.1", 53 | "ember-cli-terser": "^4.0.2", 54 | "ember-decorators": "^6.0.0", 55 | "ember-decorators-polyfill": "^1.1.5", 56 | "ember-disable-prototype-extensions": "^1.1.3", 57 | "ember-export-application-global": "^2.0.1", 58 | "ember-load-initializers": "^2.1.2", 59 | "ember-maybe-import-regenerator": "^1.0.0", 60 | "ember-qunit": "^5.1.5", 61 | "ember-resolver": "^8.0.3", 62 | "ember-responsive": "^4.0.2", 63 | "ember-source": "~3.28.1", 64 | "ember-source-channel-url": "^3.0.0", 65 | "ember-template-lint": "^3.10.0", 66 | "ember-truth-helpers": "^3.0.0", 67 | "ember-try": "^1.4.0", 68 | "eslint": "^7.32.0", 69 | "eslint-config-prettier": "^8.3.0", 70 | "eslint-plugin-ember": "^10.5.7", 71 | "eslint-plugin-node": "^11.1.0", 72 | "eslint-plugin-prettier": "^4.0.0", 73 | "eslint-plugin-qunit": "^6.2.0", 74 | "lerna-changelog": "^1.0.1", 75 | "loader.js": "^4.7.0", 76 | "npm-run-all": "^4.1.5", 77 | "prettier": "^2.4.1", 78 | "qunit": "^2.17.2", 79 | "qunit-dom": "^2.0.0", 80 | "release-it": "^13.6.6", 81 | "release-it-lerna-changelog": "^2.3.0", 82 | "webpack": "^5.58.2" 83 | }, 84 | "keywords": [ 85 | "ember-addon", 86 | "ember", 87 | "viewport", 88 | "intersection observer", 89 | "lazy load", 90 | "scrollspy" 91 | ], 92 | "ember": { 93 | "edition": "octane" 94 | }, 95 | "ember-addon": { 96 | "configPath": "tests/dummy/config" 97 | }, 98 | "volta": { 99 | "node": "14.18.1" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/unit/utils/is-in-viewport-test.js: -------------------------------------------------------------------------------- 1 | import isInViewport from 'ember-in-viewport/utils/is-in-viewport'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | let fakeRectNotInViewport, 6 | fakeRectInViewport, 7 | fakeWindow, 8 | fakeNoTolerance, 9 | fakeTolerance; 10 | 11 | module('Unit | Utility | is in viewport', function (hooks) { 12 | setupTest(hooks); 13 | 14 | hooks.beforeEach(function () { 15 | fakeRectNotInViewport = { 16 | top: 450, 17 | left: 150, 18 | bottom: 550, 19 | right: 1130, 20 | height: 1, 21 | width: 1, 22 | }; 23 | 24 | fakeRectInViewport = { 25 | top: 300, 26 | left: 150, 27 | bottom: 400, 28 | right: 1130, 29 | height: 1, 30 | width: 1, 31 | }; 32 | 33 | fakeWindow = { 34 | innerHeight: 400, 35 | innerWidth: 1280, 36 | }; 37 | 38 | fakeNoTolerance = { 39 | top: 0, 40 | left: 0, 41 | bottom: 0, 42 | right: 0, 43 | }; 44 | 45 | fakeTolerance = { 46 | top: 200, 47 | bottom: 200, 48 | }; 49 | }); 50 | 51 | test('returns true if dimensions are within viewport', function (assert) { 52 | const { innerHeight, innerWidth } = fakeWindow; 53 | const result = isInViewport( 54 | fakeRectInViewport, 55 | innerHeight, 56 | innerWidth, 57 | fakeNoTolerance 58 | ); 59 | assert.ok(result); 60 | }); 61 | 62 | test('returns false if dimensions not within viewport', function (assert) { 63 | const { innerHeight, innerWidth } = fakeWindow; 64 | const result = isInViewport( 65 | fakeRectNotInViewport, 66 | innerHeight, 67 | innerWidth, 68 | fakeNoTolerance 69 | ); 70 | assert.false(result); 71 | }); 72 | 73 | test('returns true if dimensions not within viewport but within tolerance', function (assert) { 74 | const { innerHeight, innerWidth } = fakeWindow; 75 | const result = isInViewport( 76 | fakeRectNotInViewport, 77 | innerHeight, 78 | innerWidth, 79 | fakeTolerance 80 | ); 81 | assert.ok(result); 82 | }); 83 | 84 | test('returns true if rect with subpixel height is within viewport', function (assert) { 85 | const innerHeight = 400; 86 | const innerWidth = 1280; 87 | const fakeRectWithSubpixelsInViewport = { 88 | top: 300, 89 | left: 150, 90 | bottom: 400.4, 91 | right: 1130, 92 | height: 1, 93 | width: 1, 94 | }; 95 | const result = isInViewport( 96 | fakeRectWithSubpixelsInViewport, 97 | innerHeight, 98 | innerWidth, 99 | fakeNoTolerance 100 | ); 101 | assert.ok(result); 102 | }); 103 | 104 | test('returns true if rect with subpixel width is within viewport', function (assert) { 105 | const innerHeight = 400; 106 | const innerWidth = 1280; 107 | const fakeRectWithSubpixelsInViewport = { 108 | top: 300, 109 | left: 150, 110 | bottom: 400, 111 | right: 1280.4, 112 | height: 1, 113 | width: 1, 114 | }; 115 | const result = isInViewport( 116 | fakeRectWithSubpixelsInViewport, 117 | innerHeight, 118 | innerWidth, 119 | fakeNoTolerance 120 | ); 121 | assert.ok(result); 122 | }); 123 | 124 | test('returns false if rect with subpixel height is not within viewport', function (assert) { 125 | const innerHeight = 400; 126 | const innerWidth = 1280; 127 | const fakeRectWithSubpixelsInViewport = { 128 | top: 300, 129 | left: 150, 130 | bottom: 400.8, 131 | right: 1130, 132 | height: 0, 133 | width: 0, 134 | }; 135 | const result = isInViewport( 136 | fakeRectWithSubpixelsInViewport, 137 | innerHeight, 138 | innerWidth, 139 | fakeNoTolerance 140 | ); 141 | assert.notOk(result); 142 | }); 143 | 144 | test('returns false if rect with subpixel width is not within viewport', function (assert) { 145 | const innerHeight = 400; 146 | const innerWidth = 1280; 147 | const fakeRectWithSubpixelsInViewport = { 148 | top: 300, 149 | left: 150, 150 | bottom: 400, 151 | right: 1280.7, 152 | height: 0, 153 | width: 0, 154 | }; 155 | const result = isInViewport( 156 | fakeRectWithSubpixelsInViewport, 157 | innerHeight, 158 | innerWidth, 159 | fakeNoTolerance 160 | ); 161 | assert.notOk(result); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /addon/-private/raf-admin.js: -------------------------------------------------------------------------------- 1 | import RafPool from 'raf-pool'; 2 | import isInViewport from 'ember-in-viewport/utils/is-in-viewport'; 3 | 4 | /** 5 | * ensure use on requestAnimationFrame, no matter how many components 6 | * on the page are using this class 7 | * 8 | * @class RAFAdmin 9 | */ 10 | export default class RAFAdmin { 11 | /** @private **/ 12 | constructor() { 13 | this._rafPool = new RafPool(); 14 | this.elementRegistry = new WeakMap(); 15 | } 16 | 17 | add(...args) { 18 | return this._rafPool.add(...args); 19 | } 20 | 21 | flush() { 22 | return this._rafPool.flush(); 23 | } 24 | 25 | remove(...args) { 26 | return this._rafPool.remove(...args); 27 | } 28 | 29 | reset(...args) { 30 | this._rafPool.reset(...args); 31 | this._rafPool.stop(...args); 32 | } 33 | 34 | /** 35 | * We provide our own element registry to add callbacks the user creates 36 | * 37 | * @method addEnterCallback 38 | * @param {HTMLElement} element 39 | * @param {Function} enterCallback 40 | */ 41 | addEnterCallback(element, enterCallback) { 42 | this.elementRegistry.set( 43 | element, 44 | Object.assign({}, this.elementRegistry.get(element), { enterCallback }) 45 | ); 46 | } 47 | 48 | /** 49 | * We provide our own element registry to add callbacks the user creates 50 | * 51 | * @method addExitCallback 52 | * @param {HTMLElement} element 53 | * @param {Function} exitCallback 54 | */ 55 | addExitCallback(element, exitCallback) { 56 | this.elementRegistry.set( 57 | element, 58 | Object.assign({}, this.elementRegistry.get(element), { exitCallback }) 59 | ); 60 | } 61 | } 62 | 63 | /** 64 | * This is a recursive function that adds itself to raf-pool to be executed on a set schedule 65 | * 66 | * @method startRAF 67 | * @param {HTMLElement} element 68 | * @param {Object} configurationOptions 69 | * @param {Function} enterCallback 70 | * @param {Function} exitCallback 71 | * @param {Function} addRAF 72 | * @param {Function} removeRAF 73 | */ 74 | export function startRAF( 75 | element, 76 | { scrollableArea, viewportTolerance, viewportSpy = false }, 77 | enterCallback, 78 | exitCallback, 79 | addRAF, // bound function from service to add elementId to raf pool 80 | removeRAF // bound function from service to remove elementId to raf pool 81 | ) { 82 | const domScrollableArea = 83 | typeof scrollableArea === 'string' && scrollableArea 84 | ? document.querySelector(scrollableArea) 85 | : scrollableArea instanceof HTMLElement 86 | ? scrollableArea 87 | : undefined; 88 | 89 | const height = domScrollableArea 90 | ? domScrollableArea.offsetHeight + 91 | domScrollableArea.getBoundingClientRect().top 92 | : window.innerHeight; 93 | const width = scrollableArea 94 | ? domScrollableArea.offsetWidth + 95 | domScrollableArea.getBoundingClientRect().left 96 | : window.innerWidth; 97 | const boundingClientRect = element.getBoundingClientRect(); 98 | 99 | if (boundingClientRect) { 100 | const viewportEntered = element.getAttribute('data-in-viewport-entered'); 101 | 102 | triggerDidEnterViewport( 103 | element, 104 | isInViewport(boundingClientRect, height, width, viewportTolerance), 105 | viewportSpy, 106 | enterCallback, 107 | exitCallback, 108 | viewportEntered 109 | ); 110 | 111 | if (viewportSpy || viewportEntered !== 'true') { 112 | // recursive 113 | // add to pool of requestAnimationFrame listeners and executed on set schedule 114 | addRAF( 115 | startRAF.bind( 116 | this, 117 | element, 118 | { scrollableArea, viewportTolerance, viewportSpy }, 119 | enterCallback, 120 | exitCallback, 121 | addRAF, 122 | removeRAF 123 | ) 124 | ); 125 | } else { 126 | removeRAF(); 127 | } 128 | } 129 | } 130 | 131 | function triggerDidEnterViewport( 132 | element, 133 | hasEnteredViewport, 134 | viewportSpy, 135 | enterCallback, 136 | exitCallback, 137 | viewportEntered = false 138 | ) { 139 | const didEnter = 140 | (!viewportEntered || viewportEntered === 'false') && hasEnteredViewport; 141 | const didLeave = viewportEntered === 'true' && !hasEnteredViewport; 142 | 143 | if (didEnter) { 144 | element.setAttribute('data-in-viewport-entered', true); 145 | enterCallback(); 146 | } 147 | 148 | if (didLeave) { 149 | exitCallback(); 150 | 151 | // reset so we can call again 152 | if (viewportSpy) { 153 | element.setAttribute('data-in-viewport-entered', false); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /addon/modifiers/in-viewport.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | import { action } from '@ember/object'; 3 | import { inject as service } from '@ember/service'; 4 | import { DEBUG } from '@glimmer/env'; 5 | import Modifier from 'ember-modifier'; 6 | import deepEqual from 'fast-deep-equal'; 7 | import { registerDestructor } from '@ember/destroyable'; 8 | import { macroCondition, dependencySatisfies } from '@embroider/macros'; 9 | 10 | const WATCHED_ELEMENTS = DEBUG ? new WeakSet() : undefined; 11 | 12 | let modifier; 13 | 14 | if (macroCondition(dependencySatisfies('ember-modifier', '>=3.2.0 || 4.x'))) { 15 | modifier = class InViewportModifier extends Modifier { 16 | @service inViewport; 17 | 18 | name = 'in-viewport'; 19 | 20 | lastOptions; 21 | element = null; 22 | 23 | modify(element, positional, named) { 24 | this.element = element; 25 | this.positional = positional; 26 | this.named = named; 27 | this.validateArguments(); 28 | 29 | if (!this.didSetup) { 30 | this.setupWatcher(element); 31 | registerDestructor(() => this.destroyWatcher(element)); 32 | } else if (this.hasStaleOptions) { 33 | this.destroyWatcher(element); 34 | this.setupWatcher(element); 35 | } 36 | } 37 | 38 | get options() { 39 | // eslint-disable-next-line no-unused-vars 40 | const { onEnter, onExit, ...options } = this.named; 41 | return options; 42 | } 43 | 44 | get hasStaleOptions() { 45 | return !deepEqual(this.options, this.lastOptions); 46 | } 47 | 48 | validateArguments() { 49 | assert( 50 | `'{{in-viewport}}' does not accept positional parameters. Specify listeners via 'onEnter' / 'onExit'.`, 51 | this.positional.length === 0 52 | ); 53 | assert( 54 | `'{{in-viewport}}' either expects 'onEnter', 'onExit' or both to be present.`, 55 | typeof this.named.onEnter === 'function' || 56 | typeof this.named.onExit === 'function' 57 | ); 58 | } 59 | 60 | @action 61 | onEnter(...args) { 62 | if (this.named.onEnter) { 63 | this.named.onEnter.call(null, this.element, ...args); 64 | } 65 | 66 | if (!this.options.viewportSpy) { 67 | this.inViewport.stopWatching(this.element); 68 | } 69 | } 70 | 71 | @action 72 | onExit(...args) { 73 | if (this.named.onExit) { 74 | this.named.onExit.call(null, this.element, ...args); 75 | } 76 | } 77 | 78 | setupWatcher(element) { 79 | assert( 80 | `'${element}' is already being watched. Make sure that '{{in-viewport}}' is only used once on this element and that you are not calling 'inViewport.watchElement(element)' in other places.`, 81 | !WATCHED_ELEMENTS.has(element) 82 | ); 83 | if (DEBUG) WATCHED_ELEMENTS.add(element); 84 | this.inViewport.watchElement( 85 | element, 86 | this.options, 87 | this.onEnter, 88 | this.onExit 89 | ); 90 | this.lastOptions = this.options; 91 | } 92 | 93 | destroyWatcher(element) { 94 | if (DEBUG) WATCHED_ELEMENTS.delete(element); 95 | this.inViewport.stopWatching(element); 96 | } 97 | }; 98 | } else { 99 | modifier = class InViewportModifier extends Modifier { 100 | @service inViewport; 101 | 102 | name = 'in-viewport'; 103 | 104 | lastOptions; 105 | 106 | get options() { 107 | // eslint-disable-next-line no-unused-vars 108 | const { onEnter, onExit, ...options } = this.args.named; 109 | return options; 110 | } 111 | 112 | get hasStaleOptions() { 113 | return !deepEqual(this.options, this.lastOptions); 114 | } 115 | 116 | validateArguments() { 117 | assert( 118 | `'{{in-viewport}}' does not accept positional parameters. Specify listeners via 'onEnter' / 'onExit'.`, 119 | this.args.positional.length === 0 120 | ); 121 | assert( 122 | `'{{in-viewport}}' either expects 'onEnter', 'onExit' or both to be present.`, 123 | typeof this.args.named.onEnter === 'function' || 124 | typeof this.args.named.onExit === 'function' 125 | ); 126 | } 127 | 128 | @action 129 | onEnter(...args) { 130 | if (this.args.named.onEnter) { 131 | this.args.named.onEnter.call(null, this.element, ...args); 132 | } 133 | 134 | if (!this.options.viewportSpy) { 135 | this.inViewport.stopWatching(this.element); 136 | } 137 | } 138 | 139 | @action 140 | onExit(...args) { 141 | if (this.args.named.onExit) { 142 | this.args.named.onExit.call(null, this.element, ...args); 143 | } 144 | } 145 | 146 | setupWatcher() { 147 | assert( 148 | `'${this.element}' is already being watched. Make sure that '{{in-viewport}}' is only used once on this element and that you are not calling 'inViewport.watchElement(element)' in other places.`, 149 | !WATCHED_ELEMENTS.has(this.element) 150 | ); 151 | if (DEBUG) WATCHED_ELEMENTS.add(this.element); 152 | this.inViewport.watchElement( 153 | this.element, 154 | this.options, 155 | this.onEnter, 156 | this.onExit 157 | ); 158 | this.lastOptions = this.options; 159 | } 160 | 161 | destroyWatcher() { 162 | if (DEBUG) WATCHED_ELEMENTS.delete(this.element); 163 | this.inViewport.stopWatching(this.element); 164 | } 165 | 166 | didInstall() { 167 | this.setupWatcher(); 168 | } 169 | 170 | didUpdateArguments() { 171 | if (this.hasStaleOptions) { 172 | this.destroyWatcher(); 173 | this.setupWatcher(); 174 | } 175 | } 176 | 177 | didReceiveArguments() { 178 | this.validateArguments(); 179 | } 180 | 181 | willRemove() { 182 | this.destroyWatcher(); 183 | } 184 | }; 185 | } 186 | 187 | export default modifier; 188 | -------------------------------------------------------------------------------- /addon/services/in-viewport.js: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | import { set, setProperties } from '@ember/object'; 3 | import { getOwner } from '@ember/application'; 4 | import { warn } from '@ember/debug'; 5 | import { schedule } from '@ember/runloop'; 6 | import isInViewport from 'ember-in-viewport/utils/is-in-viewport'; 7 | import canUseRAF from 'ember-in-viewport/utils/can-use-raf'; 8 | import canUseIntersectionObserver from 'ember-in-viewport/utils/can-use-intersection-observer'; 9 | import ObserverAdmin from 'ember-in-viewport/-private/observer-admin'; 10 | import RAFAdmin, { startRAF } from 'ember-in-viewport/-private/raf-admin'; 11 | 12 | const noop = () => {}; 13 | 14 | /** 15 | * ensure use on requestAnimationFrame, no matter how many components 16 | * on the page are using this class 17 | * 18 | * @class InViewport 19 | * @module Ember.Service 20 | */ 21 | export default class InViewport extends Service { 22 | constructor() { 23 | super(...arguments); 24 | 25 | set(this, 'registry', new WeakMap()); 26 | 27 | let options = Object.assign( 28 | { 29 | viewportUseRAF: canUseRAF(), 30 | }, 31 | this._buildOptions() 32 | ); 33 | 34 | // set viewportUseIntersectionObserver after merging users config to avoid errors in browsers that lack support (https://github.com/DockYard/ember-in-viewport/issues/146) 35 | options = Object.assign(options, { 36 | viewportUseIntersectionObserver: canUseIntersectionObserver(), 37 | }); 38 | 39 | setProperties(this, options); 40 | } 41 | 42 | startIntersectionObserver() { 43 | this.observerAdmin = new ObserverAdmin(); 44 | } 45 | 46 | startRAF() { 47 | this.rafAdmin = new RAFAdmin(); 48 | } 49 | 50 | /** Any strategy **/ 51 | 52 | /** 53 | * @method watchElement 54 | * @param HTMLElement element 55 | * @param Object configOptions 56 | * @param Function enterCallback 57 | * @param Function exitCallback 58 | * @void 59 | */ 60 | watchElement(element, configOptions = {}, enterCallback, exitCallback) { 61 | if (this.viewportUseIntersectionObserver) { 62 | if (!this.observerAdmin) { 63 | this.startIntersectionObserver(); 64 | } 65 | const observerOptions = this.buildObserverOptions(configOptions); 66 | 67 | schedule( 68 | 'afterRender', 69 | this, 70 | this.setupIntersectionObserver, 71 | element, 72 | observerOptions, 73 | enterCallback, 74 | exitCallback 75 | ); 76 | } else { 77 | if (!this.rafAdmin) { 78 | this.startRAF(); 79 | } 80 | schedule( 81 | 'afterRender', 82 | this, 83 | this._startRaf, 84 | element, 85 | configOptions, 86 | enterCallback, 87 | exitCallback 88 | ); 89 | } 90 | 91 | return { 92 | onEnter: this.addEnterCallback.bind(this, element), 93 | onExit: this.addExitCallback.bind(this, element), 94 | }; 95 | } 96 | 97 | /** 98 | * @method addEnterCallback 99 | * @void 100 | */ 101 | addEnterCallback(element, enterCallback) { 102 | if (this.viewportUseIntersectionObserver) { 103 | this.observerAdmin.addEnterCallback(element, enterCallback); 104 | } else { 105 | this.rafAdmin.addEnterCallback(element, enterCallback); 106 | } 107 | } 108 | 109 | /** 110 | * @method addExitCallback 111 | * @void 112 | */ 113 | addExitCallback(element, exitCallback) { 114 | if (this.viewportUseIntersectionObserver) { 115 | this.observerAdmin.addExitCallback(element, exitCallback); 116 | } else { 117 | this.rafAdmin.addExitCallback(element, exitCallback); 118 | } 119 | } 120 | 121 | /** IntersectionObserver **/ 122 | 123 | /** 124 | * In order to track elements and the state that comes with them, we need to keep track 125 | * of elements in order to get at them at a later time, specifically to unobserve 126 | * 127 | * @method addToRegistry 128 | * @void 129 | */ 130 | addToRegistry(element, observerOptions) { 131 | if (this.registry) { 132 | this.registry.set(element, { observerOptions }); 133 | } else { 134 | warn('in-viewport Service has already been destroyed'); 135 | } 136 | } 137 | 138 | /** 139 | * @method setupIntersectionObserver 140 | * @param HTMLElement element 141 | * @param Object observerOptions 142 | * @param Function enterCallback 143 | * @param Function exitCallback 144 | * @void 145 | */ 146 | setupIntersectionObserver( 147 | element, 148 | observerOptions, 149 | enterCallback, 150 | exitCallback 151 | ) { 152 | if (this.isDestroyed || this.isDestroying) { 153 | return; 154 | } 155 | 156 | this.addToRegistry(element, observerOptions); 157 | 158 | this.observerAdmin.add( 159 | element, 160 | observerOptions, 161 | enterCallback, 162 | exitCallback 163 | ); 164 | } 165 | 166 | buildObserverOptions({ 167 | intersectionThreshold = 0, 168 | scrollableArea = null, 169 | viewportTolerance = {}, 170 | }) { 171 | const domScrollableArea = 172 | typeof scrollableArea === 'string' && scrollableArea 173 | ? document.querySelector(scrollableArea) 174 | : scrollableArea instanceof HTMLElement 175 | ? scrollableArea 176 | : undefined; 177 | 178 | // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 179 | // IntersectionObserver takes either a Document Element or null for `root` 180 | const { top = 0, left = 0, bottom = 0, right = 0 } = viewportTolerance; 181 | return { 182 | root: domScrollableArea, 183 | rootMargin: `${top}px ${right}px ${bottom}px ${left}px`, 184 | threshold: intersectionThreshold, 185 | }; 186 | } 187 | 188 | unobserveIntersectionObserver(target) { 189 | if (!target) { 190 | return; 191 | } 192 | 193 | const registeredTarget = this.registry.get(target); 194 | if (typeof registeredTarget === 'object') { 195 | this.observerAdmin.unobserve(target, registeredTarget.observerOptions); 196 | } 197 | } 198 | 199 | /** RAF **/ 200 | 201 | addRAF(elementId, fn) { 202 | this.rafAdmin.add(elementId, fn); 203 | } 204 | 205 | removeRAF(elementId) { 206 | if (this.rafAdmin) { 207 | this.rafAdmin.remove(elementId); 208 | } 209 | } 210 | 211 | isInViewport(...args) { 212 | return isInViewport(...args); 213 | } 214 | 215 | /** other **/ 216 | stopWatching(target) { 217 | if (this.observerAdmin) { 218 | this.unobserveIntersectionObserver(target); 219 | } 220 | if (this.rafAdmin) { 221 | this.removeRAF(target); 222 | } 223 | } 224 | 225 | willDestroy() { 226 | set(this, 'registry', null); 227 | if (this.observerAdmin) { 228 | this.observerAdmin.destroy(); 229 | set(this, 'observerAdmin', null); 230 | } 231 | if (this.rafAdmin) { 232 | this.rafAdmin.reset(); 233 | set(this, 'rafAdmin', null); 234 | } 235 | } 236 | 237 | _buildOptions(defaultOptions = {}) { 238 | const owner = getOwner(this); 239 | 240 | if (owner) { 241 | return Object.assign(defaultOptions, owner.lookup('config:in-viewport')); 242 | } 243 | } 244 | 245 | _startRaf(element, configOptions, enterCallback, exitCallback) { 246 | if (this.isDestroyed || this.isDestroying) { 247 | return; 248 | } 249 | 250 | enterCallback = enterCallback || noop; 251 | exitCallback = exitCallback || noop; 252 | 253 | // this isn't using the same functions as the RAFAdmin, but that is b/c it is a bit harder to unwind. 254 | // So just rewrote it with pure functions for now 255 | startRAF( 256 | element, 257 | configOptions, 258 | enterCallback, 259 | exitCallback, 260 | this.addRAF.bind(this, element.id), 261 | this.removeRAF.bind(this, element.id) 262 | ); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /tests/acceptance/infinity-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupApplicationTest } from 'ember-qunit'; 3 | import { 4 | find, 5 | findAll, 6 | visit, 7 | settled, 8 | waitFor, 9 | waitUntil, 10 | } from '@ember/test-helpers'; 11 | 12 | module('Acceptance | infinity-scrollable', function (hooks) { 13 | setupApplicationTest(hooks); 14 | 15 | hooks.beforeEach(function () { 16 | // bring testem window and the browser up to the top. 17 | document.getElementById('ember-testing-container').scrollTop = 0; 18 | }); 19 | 20 | test('IntersectionObserver Component fetches more data when scrolled into viewport', async function (assert) { 21 | await visit('/infinity-scrollable'); 22 | 23 | assert.equal(findAll('.infinity-svg').length, 10); 24 | assert.equal( 25 | findAll('.infinity-scrollable.inactive').length, 26 | 1, 27 | 'component is inactive before fetching more data' 28 | ); 29 | document.querySelector('.infinity-scrollable').scrollIntoView(false); 30 | 31 | await waitFor('.infinity-scrollable.inactive'); 32 | await waitUntil(() => { 33 | return findAll('.infinity-svg').length === 20; 34 | }); 35 | 36 | assert.equal(findAll('.infinity-svg').length, 20); 37 | }); 38 | 39 | test('works with in-viewport modifier', async function (assert) { 40 | await visit('/infinity-built-in-modifiers'); 41 | 42 | assert.equal(findAll('.infinity-item').length, 10, 'has items to start'); 43 | 44 | document.querySelector('.infinity-item-9').scrollIntoView(false); 45 | 46 | await waitUntil( 47 | () => { 48 | return findAll('.infinity-item').length === 20; 49 | }, 50 | { timeoutMessage: 'did not find all items in time' } 51 | ); 52 | 53 | await settled(); 54 | 55 | assert.equal( 56 | findAll('.infinity-item').length, 57 | 20, 58 | 'after infinity has more items' 59 | ); 60 | assert.equal( 61 | find('h1').textContent.trim(), 62 | '{{in-viewport}} modifier', 63 | 'has title' 64 | ); 65 | 66 | document.querySelector('.infinity-item-19').scrollIntoView(false); 67 | 68 | await waitUntil( 69 | () => { 70 | return findAll('.infinity-item').length === 30; 71 | }, 72 | { timeoutMessage: 'did not find all items in time' } 73 | ); 74 | 75 | await settled(); 76 | 77 | assert.equal( 78 | findAll('.infinity-item').length, 79 | 30, 80 | 'after infinity has more items' 81 | ); 82 | assert.equal( 83 | find('h1').textContent.trim(), 84 | '{{in-viewport}} modifier', 85 | 'has title' 86 | ); 87 | }); 88 | 89 | test('works with in-viewport modifier (rAF)', async function (assert) { 90 | let inViewportService = this.owner.lookup('service:in-viewport'); 91 | 92 | inViewportService.set('viewportUseIntersectionObserver', false); 93 | 94 | await visit('/infinity-built-in-modifiers'); 95 | 96 | assert.equal(findAll('.infinity-item').length, 10, 'has items to start'); 97 | 98 | document.querySelector('.infinity-item-9').scrollIntoView(false); 99 | 100 | await waitUntil( 101 | () => { 102 | return findAll('.infinity-item').length === 20; 103 | }, 104 | { timeoutMessage: 'did not find all items in time' } 105 | ); 106 | 107 | await settled(); 108 | 109 | assert.equal( 110 | findAll('.infinity-item').length, 111 | 20, 112 | 'after infinity has more items' 113 | ); 114 | }); 115 | 116 | test('ember-in-viewport works with classes', async function (assert) { 117 | await visit('/infinity-class'); 118 | 119 | assert.equal(findAll('.infinity-class-item').length, 20); 120 | document.querySelector('#loader').scrollIntoView(false); 121 | 122 | await waitUntil(() => { 123 | return findAll('.infinity-class-item').length === 40; 124 | }); 125 | 126 | assert.equal(findAll('.infinity-class-item').length, 40); 127 | }); 128 | 129 | test('IntersectionObserver Component fetches more data when left to right scrolling', async function (assert) { 130 | await visit('/infinity-right-left'); 131 | 132 | assert.equal(findAll('.infinity-svg').length, 10); 133 | assert.equal( 134 | findAll('.infinity-scrollable.inactive').length, 135 | 1, 136 | 'component is inactive before fetching more data' 137 | ); 138 | document.querySelector('.infinity-scrollable').scrollIntoView(false); 139 | 140 | await waitFor('.infinity-scrollable.inactive'); 141 | 142 | // assert.equal(findAll('.infinity-svg').length, 20); 143 | }); 144 | 145 | test('rAF Component fetches more data when scrolled into viewport', async function (assert) { 146 | await visit('/infinity-scrollable-raf'); 147 | 148 | assert.equal(findAll('.infinity-svg-rAF').length, 10); 149 | assert.equal( 150 | findAll('.infinity-scrollable-rAF.inactive').length, 151 | 1, 152 | 'component is inactive before fetching more data' 153 | ); 154 | document.querySelector('.infinity-scrollable-rAF').scrollIntoView(false); 155 | 156 | await waitUntil(() => { 157 | return findAll('.infinity-svg-rAF').length === 20; 158 | }); 159 | await waitFor('.infinity-scrollable-rAF.inactive'); 160 | 161 | assert.equal(findAll('.infinity-svg-rAF').length, 20); 162 | assert.equal( 163 | findAll('.infinity-scrollable-rAF.inactive').length, 164 | 1, 165 | 'component is inactive after fetching more data' 166 | ); 167 | }); 168 | 169 | test('rAF (second) component does not fetch after first call (viewportSpy is false)', async function (assert) { 170 | await visit('/infinity-scrollable-raf'); 171 | 172 | assert.equal(findAll('.infinity-svg-rAF-bottom').length, 10); 173 | assert.equal( 174 | findAll('.infinity-scrollable-rAF-bottom.inactive').length, 175 | 1, 176 | 'component is inactive before fetching more data' 177 | ); 178 | document 179 | .querySelector('.infinity-scrollable-rAF-bottom') 180 | .scrollIntoView(false); 181 | 182 | await waitUntil(() => { 183 | return findAll('.infinity-svg-rAF-bottom').length === 20; 184 | }); 185 | await waitFor('.infinity-scrollable-rAF-bottom.inactive'); 186 | 187 | document 188 | .querySelector('.infinity-scrollable-rAF-bottom') 189 | .scrollIntoView(false); 190 | 191 | await waitUntil(() => { 192 | // one tick is enough to check 193 | return findAll('.infinity-svg-rAF-bottom').length === 20; 194 | }); 195 | await waitFor('.infinity-scrollable-rAF-bottom.inactive'); 196 | }); 197 | 198 | test('scrollEvent Component fetches more data when scrolled into viewport', async function (assert) { 199 | await visit('/infinity-scrollable-scrollevent'); 200 | 201 | assert.equal(findAll('.infinity-svg-scrollEvent').length, 10); 202 | assert.equal( 203 | findAll('.infinity-scrollable-scrollEvent.inactive').length, 204 | 1, 205 | 'component is inactive before fetching more data' 206 | ); 207 | await document 208 | .querySelector('.infinity-scrollable-scrollEvent') 209 | .scrollIntoView(false); 210 | 211 | await waitUntil(() => { 212 | return findAll('.infinity-svg-scrollEvent').length === 20; 213 | }); 214 | 215 | assert.equal(findAll('.infinity-svg-scrollEvent').length, 20); 216 | assert.ok( 217 | find('.infinity-scrollable-scrollEvent.active'), 218 | 'component is still active after fetching more data' 219 | ); 220 | // scroll 1px to trigger inactive state 221 | let elem = document.getElementsByClassName('list-scrollEvent')[0]; 222 | elem.scrollTop = elem.scrollTop + 5; 223 | 224 | await waitUntil(() => { 225 | return find('.infinity-scrollable-scrollEvent.inactive'); 226 | }); 227 | 228 | assert.ok( 229 | find('.infinity-scrollable-scrollEvent.inactive'), 230 | 'component is inactive after scrolling' 231 | ); 232 | }); 233 | 234 | test('works with custom elements', async function (assert) { 235 | await visit('/infinity-custom-element'); 236 | 237 | assert.equal(findAll('.infinity-item').length, 10, 'has items to start'); 238 | 239 | document.querySelector('custom-sentinel').scrollIntoView(false); 240 | 241 | await waitUntil( 242 | () => { 243 | return findAll('.infinity-item').length === 20; 244 | }, 245 | { timeoutMessage: 'did not find all items in time' } 246 | ); 247 | 248 | await settled(); 249 | 250 | // assert.equal(findAll('.infinity-item').length, 20, 'after infinity has more items'); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /tests/dummy/app/components/dummy-artwork.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { htmlSafe } from '@ember/string'; 3 | import { action, set } from '@ember/object'; 4 | import { inject as service } from '@ember/service'; 5 | import { artworkProfiles, artworkFallbacks, viewports } from '../-config'; 6 | import { guidFor } from '@ember/object/internals'; 7 | import ENV from 'dummy/config/environment'; 8 | 9 | /** 10 | * This function generates a value for the `srcset` attribute 11 | * based on a URL and image options. 12 | * 13 | * where options: 14 | * - `{w}` is the width placeholder 15 | * - `{h}` is the height placeholder 16 | * - `{c}` is the crop placeholder 17 | * - `{f}` is the file type placeholder 18 | * 19 | * The options object specified is expected to have `width`, 20 | * `height`, `crop`, and `fileType` key/value pairs. 21 | * 22 | * @method buildSrcset 23 | * @param {String} rawURL The raw URL 24 | * @param {Object} options The image options 25 | * @return {String} The `srcset` attribute value 26 | * @public 27 | */ 28 | export function buildSrcset(url, options, pixelDensity = 1) { 29 | return `${url} ${options.width * pixelDensity}w`; 30 | } 31 | 32 | /** 33 | * #### Usage 34 | * 35 | * ```hbs 36 | * 37 | * ``` 38 | * 39 | * @class DummyArtwork 40 | * @module Components 41 | * @extends {Ember.Component} 42 | * @public 43 | */ 44 | export default class DummyArtwork extends Component { 45 | @service inViewport; 46 | @service media; 47 | 48 | rootURL = ENV.rootURL; 49 | 50 | /** 51 | * Provide an `alt` attribute for the `` tag. Default is an empty string. 52 | * 53 | * @property alt 54 | * @type String 55 | * @public 56 | */ 57 | 58 | /** 59 | * @property isDownloaded 60 | * @type {Boolean} 61 | * @default false 62 | * @public 63 | */ 64 | isDownloaded = false; 65 | 66 | /** 67 | * @property isErrored 68 | * @type {Boolean} 69 | * @default false 70 | * @public 71 | */ 72 | isErrored = false; 73 | 74 | /** 75 | * @property fileType 76 | * @type String 77 | * @public 78 | */ 79 | fileType = 'jpg'; 80 | 81 | /** 82 | * @property lazyLoad 83 | * @type {Boolean} 84 | * @default true 85 | * @public 86 | */ 87 | lazyLoad = true; 88 | 89 | /** 90 | * The value to be used for background-color CSS property 91 | * when addBgColor is true. This will override 92 | * any property included in the artwork data. 93 | * @property overrideBgColor 94 | * @type {String} 95 | * @public 96 | */ 97 | overrideBgColor; 98 | 99 | /** 100 | * Indicates if a background color should be added to 101 | * display while loading 102 | * 103 | * @property addBgColor 104 | * @type {Boolean} 105 | * @public 106 | */ 107 | addBgColor; 108 | 109 | constructor(...args) { 110 | super(...args); 111 | 112 | // for use in template 113 | this.boundOnError = this.onError.bind(this); 114 | this.boundOnLoad = this.onLoad.bind(this); 115 | set(this, 'guid', guidFor(this)); 116 | } 117 | 118 | /** 119 | * 120 | * @property userInitials 121 | * @type {String} 122 | * @public 123 | */ 124 | get userInitials() { 125 | return this.actualArtwork.userInitials; 126 | } 127 | 128 | /** 129 | * 130 | * @property isFallbackArtwork 131 | * @type {Boolean} 132 | * @public 133 | */ 134 | get isFallbackArtwork() { 135 | return this.actualArtwork.isFallback; 136 | } 137 | 138 | /** 139 | * 140 | * @property isUserMonogram 141 | * @type {Boolean} 142 | * @public 143 | */ 144 | get isUserMonogram() { 145 | return this.isFallbackArtwork && !!this.userInitials; 146 | } 147 | 148 | /** 149 | * @property artworkClasses 150 | * @type {String} 151 | * @public 152 | */ 153 | get artworkClasses() { 154 | let classes = this.class || ''; 155 | if (this.isDownloaded) { 156 | classes += ' dummy-artwork--downloaded '; 157 | } 158 | 159 | return classes.trim(); 160 | } 161 | 162 | /** 163 | * @property height 164 | * @type {String|Number} 165 | * @public 166 | */ 167 | get height() { 168 | const [viewport = 'medium'] = this.media.matches; 169 | if (this.profiles && this.profiles[viewport]) { 170 | return this.profiles[viewport].height; 171 | } 172 | 173 | return this.profiles.large && this.profiles.large.height; 174 | } 175 | 176 | /** 177 | * @property width 178 | * @type {String|Number} 179 | * @public 180 | */ 181 | get width() { 182 | const [viewport = 'medium'] = this.media.matches; 183 | if (this.profiles && this.profiles[viewport]) { 184 | return this.profiles[viewport].width; 185 | } 186 | 187 | // no profile if no artwork 188 | return this.profiles.large && this.profiles.large.width; 189 | } 190 | 191 | /** 192 | * The background color inline style for the artwork. 193 | * This will be visible while the image is loading. 194 | * 195 | * @property bgColor 196 | * @type String 197 | * @public 198 | */ 199 | get bgColor() { 200 | if (!this.actualArtwork || this.actualArtwork.hasAlpha) { 201 | return htmlSafe(''); 202 | } 203 | const { overrideBgColor, addBgColor } = this; 204 | const bgColor = overrideBgColor || this.actualArtwork.bgColor; 205 | if (addBgColor && bgColor) { 206 | return `#${bgColor}`; 207 | } 208 | } 209 | 210 | get imgBgColor() { 211 | if (this.bgColor) { 212 | return htmlSafe(`background-color: ${this.bgColor};`); 213 | } 214 | } 215 | 216 | /** 217 | * @property aspectRatio The aspect ratio of the artwork from the config 218 | * @type Number 219 | * @private 220 | */ 221 | get aspectRatio() { 222 | return this.width / this.height; 223 | } 224 | 225 | /** 226 | * This is the aspect ratio of the artwork itself, as opposed to the desired width/height 227 | * passed in by the consumer. 228 | * 229 | * @property mediaQueries The aspect ratio of the artwork from the server 230 | * @type number 231 | * @private 232 | */ 233 | get mediaQueries() { 234 | return viewports 235 | .map(({ mediaQueryStrict, name }) => { 236 | if (!this.profiles[name]) { 237 | return; 238 | } 239 | return `${mediaQueryStrict} ${this.profiles[name].width}px`; 240 | }) 241 | .filter(Boolean) 242 | .join(', ') 243 | .trim(); 244 | } 245 | 246 | /** 247 | * An artworkProfile may be a string or object. 248 | * There may not be different viewport defined sizes for an artwork profile. 249 | * As a result, we dont want to avoid duplicate * work and tell the browser that the same size 250 | * exists for each lg/medium/small viewport. 251 | * 252 | * { large: { height, width, crop }, medium: { height, width, crop }, small: { ... } } 253 | * 254 | * or just this 255 | * 256 | * e.g. { large: { height, width, crop } } 257 | * 258 | * @property profile 259 | * @type Object 260 | * @public 261 | */ 262 | get profile() { 263 | let profile = {}; 264 | if (typeof this.args.artworkProfile === 'string') { 265 | profile = artworkProfiles[this.args.artworkProfile]; 266 | } else if (typeof this.args.artworkProfile === 'object') { 267 | profile = this.args.artworkProfile; 268 | } 269 | 270 | return profile; 271 | } 272 | 273 | /** 274 | * @property profiles 275 | * @type Object 276 | * @public 277 | */ 278 | get profiles() { 279 | // eslint-disable-next-line arrow-body-style 280 | return viewports.reduce((acc, view) => { 281 | // the artwork-profile might not define a size at a specific viewport defined in app/breakpoints.js 282 | if (this.profile[view.name]) { 283 | const { height, width, crop } = this.profile[view.name]; 284 | acc[view.name] = { width, height, crop }; 285 | } 286 | 287 | return acc; 288 | }, {}); 289 | } 290 | 291 | /** 292 | * we render the fallback src directly in the image with no srcset 293 | * 294 | * @property fallbackSrc 295 | * @type String 296 | * @private 297 | */ 298 | get fallbackSrc() { 299 | const { 300 | actualArtwork: { url, isFallback = false }, 301 | } = this; 302 | if (isFallback) { 303 | return url; 304 | } 305 | } 306 | 307 | /** 308 | * @property srcset 309 | * @type String 310 | * @private 311 | */ 312 | get srcset() { 313 | const { 314 | actualArtwork: { url, isFallback = false }, 315 | } = this; 316 | return [1, 2] 317 | .map((pixelDensity) => 318 | viewports.map(({ name }) => { 319 | const settings = Object.assign( 320 | {}, 321 | { fileType: this.fileType }, 322 | this.profiles[name] 323 | ); 324 | // Build a srcset from patterned URL 325 | if (isFallback) { 326 | return; 327 | } 328 | return buildSrcset(url, settings, pixelDensity); 329 | }) 330 | ) 331 | .join(', '); 332 | } 333 | 334 | /** 335 | * @property actualArtwork 336 | * @type Object 337 | * @private 338 | */ 339 | get actualArtwork() { 340 | const { url } = this.args.artwork || {}; 341 | const { fallbackArtwork, isErrored } = this; 342 | 343 | if ((!url && fallbackArtwork) || isErrored) { 344 | return Object.assign({}, fallbackArtwork, { isFallback: true }); 345 | } 346 | 347 | return this.args.artwork; 348 | } 349 | 350 | /** 351 | * If the fallback profile provided exists, we find the corresponding 352 | * fallback artwork object from the app config. This is used whenever 353 | * the main artwork object is missing or invalid. 354 | * 355 | * @property fallbackArtwork 356 | * @type object 357 | * @private 358 | */ 359 | get fallbackArtwork() { 360 | const { fallbackProfile } = this; 361 | if (typeof fallbackProfile === 'object') { 362 | return fallbackProfile; 363 | } 364 | const fallbackArtwork = artworkFallbacks[fallbackProfile]; 365 | 366 | if (fallbackArtwork) { 367 | return fallbackArtwork; 368 | } 369 | 370 | return null; 371 | } 372 | 373 | /** 374 | * Inline style to properly scale the img element. 375 | * 376 | * @property imgStyle 377 | * @type String 378 | * @public 379 | */ 380 | get imgStyle() { 381 | return Object.keys(this.profiles) 382 | .map((name) => { 383 | const source = this.profiles[name]; 384 | let style = ''; 385 | if (source.width > 0) { 386 | style = `#${this.guid}, #${this.guid}::before { 387 | width: ${source.width}px; 388 | height: ${source.height}px; 389 | } 390 | #${this.guid}::before { 391 | padding-top: ${(source.height / source.width) * 100}%; 392 | }`; 393 | } 394 | 395 | if (source.mediaQuery && style.length > 0) { 396 | return `@media ${source.mediaQuery} { 397 | ${style} 398 | }`; 399 | } 400 | 401 | return style; 402 | }) 403 | .reverse() 404 | .join('\n'); 405 | } 406 | 407 | @action 408 | setupInViewport(element) { 409 | if (this.lazyLoad) { 410 | // find distance of top left corner of artwork to bottom of screen. Shave off 50px so user has to scroll slightly to trigger load 411 | window.requestAnimationFrame(() => { 412 | const { onEnter } = this.inViewport.watchElement(element, { 413 | viewportTolerance: { top: 200, right: 200, bottom: 200, left: 200 }, 414 | }); 415 | 416 | onEnter(this.didEnterViewport.bind(this)); 417 | }); 418 | } 419 | } 420 | 421 | /** 422 | * in-viewport hook to set src and srset based on data-* attrs 423 | * 424 | * @method didEnterViewport 425 | * @private 426 | */ 427 | didEnterViewport() { 428 | if (this.isDestroyed || this.isDestroying) { 429 | return; 430 | } 431 | 432 | this._swapSource(); 433 | this.inViewport.stopWatching(document.getElementById(this.guid)); 434 | } 435 | 436 | /** 437 | * @method onError 438 | */ 439 | onError() { 440 | if (this.isDestroyed || this.isDestroying) { 441 | return; 442 | } 443 | 444 | set(this, 'isErrored', true); 445 | } 446 | 447 | /** 448 | * @method onLoad 449 | */ 450 | onLoad() { 451 | if (this.isDestroyed || this.isDestroying) { 452 | return; 453 | } 454 | set(this, 'isDownloaded', true); 455 | } 456 | 457 | willDestroy(...args) { 458 | this.inViewport.stopWatching(document.getElementById(this.guid)); 459 | 460 | super.willDestroy(...args); 461 | } 462 | 463 | /** 464 | * swap src and srset with data attributes that hold the real src 465 | * 466 | * @method _swapSource 467 | * @private 468 | */ 469 | _swapSource() { 470 | const { lazyLoad, isDownloaded, isFallbackArtwork } = this; 471 | const element = document.getElementById(this.guid); 472 | 473 | if (lazyLoad && element && !isDownloaded && !isFallbackArtwork) { 474 | const img = element.querySelector('img'); 475 | if (img && img.dataset) { 476 | img.onload = this.onLoad.bind(this); 477 | img.srcset = img.dataset.srcset; 478 | } 479 | } 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-in-viewport 2 | *Detect if an Ember View or Component is in the viewport @ 60FPS* 3 | 4 | **[ember-in-viewport is built and maintained by DockYard, contact us for expert Ember.js consulting](https://dockyard.com/ember-consulting)**. 5 | 6 | [Read the blogpost](https://medium.com/delightful-ui-for-ember-apps/creating-an-ember-cli-addon-detecting-ember-js-components-entering-or-leaving-the-viewport-7d95ceb4f5ed) 7 | 8 | ![Download count all time](https://img.shields.io/npm/dt/ember-in-viewport.svg) 9 | [![npm version](https://badge.fury.io/js/ember-in-viewport.svg)](http://badge.fury.io/js/ember-in-viewport) 10 | [![GitHub Actions Build Status](https://img.shields.io/github/workflow/status/DockYard/ember-in-viewport/CI/master)](https://github.com/DockYard/ember-in-viewport/actions/workflows/ci.yml?query=branch%3Amaster) 11 | [![Ember Observer Score](http://emberobserver.com/badges/ember-in-viewport.svg)](http://emberobserver.com/addons/ember-in-viewport) 12 | 13 | This Ember addon adds a simple, highly performant Service or modifier to your app. This library will allow you to check if a `Component` or DOM element has entered the browser's viewport. By default, this uses the `IntersectionObserver` API if it detects it the DOM element is in your user's browser – failing which, it falls back to using `requestAnimationFrame`, then if not available, the Ember run loop and event listeners. 14 | 15 | We utilize pooling techniques to reuse Intersection Observers and rAF observers in order to make your app as performant as possible and do as little works as possible. 16 | 17 | ## Demo or examples 18 | - Lazy loading responsive images (see `dummy-artwork` for an example artwork component). Visit `http://localhost:4200/infinity-modifier` to see it in action 19 | - Dummy app (`ember serve`): https://github.com/DockYard/ember-in-viewport/tree/master/tests/dummy 20 | - Use with Ember [Modifiers](#modifiers) and [@ember/render-modifiers](https://github.com/emberjs/ember-render-modifiers) 21 | - Use with [Native Classes](#classes) 22 | - [ember-infinity](https://github.com/ember-infinity/ember-infinity) 23 | - [ember-light-table](https://github.com/offirgolan/ember-light-table) 24 | - Tracking advertisement impressions 25 | - Occlusion culling 26 | 27 | 28 | # Table of Contents 29 | 30 | - [Installation](#installation) 31 | * [Usage](#usage) 32 | + [Configuration](#configuration) 33 | + [Global options](#global-options) 34 | + [Modifiers](#modifiers) 35 | * [**IntersectionObserver**'s Browser Support](#intersectionobservers-browser-supportscrollableArea) 36 | + [Out of the box](#out-of-the-box) 37 | * [Running](#running) 38 | * [Running Tests](#running-tests) 39 | * [Building](#building) 40 | * [Legal](#legal) 41 | 42 | 43 | 44 | # Installation 45 | 46 | ``` 47 | ember install ember-in-viewport 48 | ``` 49 | 50 | ## Usage 51 | Usage is simple. First, inject the service to your component and start "watching" DOM elements. 52 | 53 | ```js 54 | import Component from '@glimmer/component'; 55 | import { action } from '@ember/object'; 56 | import { inject as service } from '@ember/service'; 57 | 58 | export default class MyClass extends Component { 59 | @service inViewport 60 | 61 | @action 62 | setupInViewport() { 63 | const loader = document.getElementById('loader'); 64 | const viewportTolerance = { bottom: 200 }; 65 | const { onEnter, _onExit } = this.inViewport.watchElement(loader, { viewportTolerance }); 66 | // pass the bound method to `onEnter` or `onExit` 67 | onEnter(this.didEnterViewport.bind(this)); 68 | } 69 | 70 | didEnterViewport() { 71 | // do some other stuff 72 | this.infinityLoad(); 73 | } 74 | 75 | willDestroy() { 76 | // need to manage cache yourself 77 | const loader = document.getElementById('loader'); 78 | this.inViewport.stopWatching(loader); 79 | 80 | super.willDestroy(...arguments); 81 | } 82 | } 83 | ``` 84 | 85 | ```hbs 86 |
    87 |
  • 88 | ... 89 |
90 |
91 | ``` 92 | 93 | You can also use [`Modifiers`](#modifiers) as well. Using modifiers cleans up the boilerplate needed and is shown in a later example. 94 | 95 | ### Configuration 96 | To use with the service based approach, simply pass in the options to `watchElement` as the second argument. 97 | 98 | ```js 99 | import Component from '@glimmer/component'; 100 | import { inject as service } from '@ember/service'; 101 | 102 | export default class MyClass extends Component { 103 | @service inViewport 104 | 105 | @action 106 | setupInViewport() { 107 | const loader = document.getElementById('loader'); 108 | 109 | const { onEnter, _onExit } = this.inViewport.watchElement( 110 | loader, 111 | { 112 | viewportTolerance: { bottom: 200 }, 113 | intersectionThreshold: 0.25, 114 | scrollableArea: '#scrollable-area' 115 | } 116 | ); 117 | } 118 | } 119 | ``` 120 | 121 | ### Global options 122 | 123 | You can set application wide defaults for `ember-in-viewport` in your app (they are still manually overridable inside of a Component). To set new defaults, just add a config object to `config/environment.js`, like so: 124 | 125 | ```js 126 | module.exports = function(environment) { 127 | var ENV = { 128 | // ... 129 | viewportConfig: { 130 | viewportUseRAF : true, 131 | viewportSpy : false, 132 | viewportListeners : [], 133 | intersectionThreshold : 0, 134 | scrollableArea : null, 135 | viewportTolerance: { 136 | top : 0, 137 | left : 0, 138 | bottom : 0, 139 | right : 0 140 | } 141 | } 142 | }; 143 | }; 144 | 145 | // Note if you want to disable right and left in-viewport triggers, set these values to `Infinity`. 146 | ``` 147 | 148 | ### Modifiers 149 | 150 | Using with [Modifiers](https://blog.emberjs.com/2019/03/06/coming-soon-in-ember-octane-part-4.html) is easy. 151 | 152 | You can either use our built in modifier `{{in-viewport}}` or a more verbose, but potentially more flexible generic modifier. Let's start with the former. 153 | 154 | 1. Use `{{in-viewport}}` modifier on target element 155 | 2. Ensure you have a callbacks in context for enter and/or exit 156 | 3. `options` are optional - see [Advanced usage (options)](#advanced-usage-options) 157 | 158 | ```hbs 159 |
    160 |
  • 161 |
  • 162 |
    163 | List sentinel 164 |
    165 |
166 | ``` 167 | 168 | This modifier is useful for a variety of scenarios where you need to watch a sentinel. With template only components, functionality like this is even more important! If you have logic that currently uses the `did-insert` modifier to start watching an element, try this one out! 169 | 170 | If you need more than our built in modifier... 171 | 172 | 1. Install [@ember/render-modifiers](https://github.com/emberjs/ember-render-modifiers) 173 | 2. Use the `did-insert` hook inside a component 174 | 3. Wire up the component like so 175 | 176 | Note - This is in lieu of a `did-enter-viewport` modifier, which we plan on adding in the future. Compared to the solution below, `did-enter-viewport` won't need a container (`this`) passed to it. But for now, to start using modifiers, this is the easy path. 177 | 178 | ```js 179 | import Component from '@glimmer/component'; 180 | import { action } from '@ember/object'; 181 | import { inject as service } from '@ember/service'; 182 | 183 | export default class MyClass extends Component { 184 | @service inViewport 185 | 186 | @action 187 | setupInViewport() { 188 | const loader = document.getElementById('loader'); 189 | const viewportTolerance = { bottom: 200 }; 190 | const { onEnter, _onExit } = this.inViewport.watchElement(loader, { viewportTolerance }); 191 | onEnter(this.didEnterViewport.bind(this)); 192 | } 193 | 194 | didEnterViewport() { 195 | // do some other stuff 196 | this.infinityLoad(); 197 | } 198 | 199 | willDestroy() { 200 | // need to manage cache yourself 201 | const loader = document.getElementById('loader'); 202 | this.inViewport.stopWatching(loader); 203 | 204 | super.willDestroy(...arguments); 205 | } 206 | } 207 | ``` 208 | 209 | ```hbs 210 |
211 | {{yield}} 212 |
213 | ``` 214 | 215 | Options as the second argument to `inViewport.watchElement` include: 216 | - `intersectionThreshold: decimal or array` 217 | 218 | Default: 0 219 | 220 | A single number or array of numbers between 0.0 and 1.0. A value of 0.0 means the target will be visible when the first pixel enters the viewport. A value of 1.0 means the entire target must be visible to fire the didEnterViewport hook. 221 | Similarily, [0, .25, .5, .75, 1] will fire didEnterViewport every 25% of the target that is visible. 222 | (https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Thresholds) 223 | 224 | Some notes: 225 | - If the target is offscreen, you will get a notification via `didExitViewport` that the target is initially offscreen. Similarily, this is possible to notify if onscreen when your site loads. 226 | - If intersectionThreshold is set to anything greater than 0, you will not see `didExitViewport` hook fired due to our use of the `isIntersecting` property. See last comment here: https://bugs.chromium.org/p/chromium/issues/detail?id=713819 for purpose of `isIntersecting` 227 | - To get around the above issue and have `didExitViewport` fire, set your `intersectionThreshold` to `[0, 1.0]`. When set to just `1.0`, when the element is 99% visible and still has isIntersecting as true, when the element leaves the viewport, the element isn't applicable to the observer anymore, so the callback isn't called again. 228 | - If your intersectionThreshold is set to 0 you will get notified if the target `didEnterViewport` and `didExitViewport` at the appropriate time. 229 | 230 | - `scrollableArea: string | HTMLElement` 231 | 232 | Default: null 233 | 234 | A CSS selector for the scrollable area. e.g. `".my-list"` 235 | 236 | - `viewportSpy: boolean` 237 | 238 | Default: `false` 239 | 240 | `viewportSpy: true` is often useful when you have "infinite lists" that need to keep loading more data. 241 | `viewportSpy: false` is often useful for one time loading of artwork, metrics, etc when the come into the viewport. 242 | 243 | If you support IE11 and detect and run logic `onExit`, then it is necessary to have this `true` to that the requestAnimationFrame watching your sentinel is not torn down. 244 | 245 | When `true`, the library will continually watch the `Component` and re-fire hooks whenever it enters or leaves the viewport. Because this is expensive, this behaviour is opt-in. When false, the intersection observer will only watch the `Component` until it enters the viewport once, and then it unbinds listeners. This reduces the load on the Ember run loop and your application. 246 | 247 | NOTE: If using IntersectionObserver (default), viewportSpy wont put too much of a tax on your application. However, for browsers (Safari < 12.1) that don't currently support IntersectionObserver, we fallback to rAF. Depending on your use case, the default of `false` may be acceptable. 248 | 249 | - `viewportTolerance: object` 250 | 251 | Default: `{ top: 0, left: 0, bottom: 0, right: 0 }` 252 | 253 | This option determines how accurately the `Component` needs to be within the viewport for it to be considered as entered. Add bottom margin to preemptively trigger didEnterViewport. 254 | 255 | For IntersectionObserver, this property interpolates to [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin). 256 | For rAF, this property will use `bottom` tolerance and measure against the height of the container to determine when to trigger didEnterViewport. 257 | 258 | Also, if your sentinel (the watched element) is a zero-height element, ensure that the sentinel actually is able to enter the viewport. 259 | 260 | 261 | ## [**IntersectionObserver**'s Browser Support](https://platform-status.mozilla.org/) 262 | 263 | ### Out of the box 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 |
Chrome51 [1]
Firefox (Gecko)55 [2]
MS Edge15
Internet ExplorerNot supported
Opera [1]38
SafariSafari Technology Preview
Chrome for Android59
Android Browser56
Opera Mobile37
303 | 304 | * [1] [Reportedly available](https://www.chromestatus.com/features/5695342691483648), it didn't trigger the events on initial load and lacks `isIntersecting` until later versions. 305 | * [2] This feature was implemented in Gecko 53.0 (Firefox 53.0 / Thunderbird 53.0 / SeaMonkey 2.50) behind the preference `dom.IntersectionObserver.enabled`. 306 | 307 | ## Running 308 | 309 | * `ember serve` 310 | * Visit your app at http://localhost:4200. 311 | 312 | ## Running Tests 313 | 314 | * `ember test` 315 | * `ember test --serve` 316 | 317 | ## Building 318 | 319 | * `ember build` 320 | 321 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 322 | 323 | ## Legal 324 | 325 | [DockYard](http://dockyard.com/ember-consulting), Inc © 2015 326 | 327 | [@dockyard](http://twitter.com/dockyard) 328 | 329 | [Licensed under the MIT license](http://www.opensource.org/licenses/mit-license.php) 330 | 331 | ## Contributors 332 | 333 | We're grateful to these wonderful contributors who've contributed to `ember-in-viewport`: 334 | 335 | [//]: contributor-faces 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | [//]: contributor-faces 366 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v4.0.0 (2021-11-29) 2 | 3 | #### :boom: Breaking Change 4 | * [#285](https://github.com/DockYard/ember-in-viewport/pull/285) Upgrade ember-auto-import to v2 ([@SergeAstapov](https://github.com/SergeAstapov)) 5 | 6 | #### :rocket: Enhancement 7 | * [#291](https://github.com/DockYard/ember-in-viewport/pull/291) Add 3.28 to test suite ([@snewcomer](https://github.com/snewcomer)) 8 | * [#290](https://github.com/DockYard/ember-in-viewport/pull/290) Add 3.28 testing scenario ([@snewcomer](https://github.com/snewcomer)) 9 | * [#289](https://github.com/DockYard/ember-in-viewport/pull/289) Bump ember-modifier to v3 and allow either v2 or v3 ([@SergeAstapov](https://github.com/SergeAstapov)) 10 | * [#288](https://github.com/DockYard/ember-in-viewport/pull/288) Update Build Status badge: Travis -> GH Actions ([@SergeAstapov](https://github.com/SergeAstapov)) 11 | * [#287](https://github.com/DockYard/ember-in-viewport/pull/287) Update npmignore file ([@SergeAstapov](https://github.com/SergeAstapov)) 12 | * [#284](https://github.com/DockYard/ember-in-viewport/pull/284) Add changelog via lerna-changelog ([@SergeAstapov](https://github.com/SergeAstapov)) 13 | 14 | #### Committers: 2 15 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 16 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 17 | 18 | 19 | ## v3.10.3 (2021-10-14) 20 | 21 | #### :bug: Bug Fix 22 | * [#278](https://github.com/DockYard/ember-in-viewport/pull/278) [Bug]: Pass the intersectionObserverEntry to callbacks of {{in-vieport}} modifier ([@cibernox](https://github.com/cibernox)) 23 | 24 | #### Committers: 1 25 | - Miguel Camba ([@cibernox](https://github.com/cibernox)) 26 | 27 | 28 | ## v3.10.2 (2021-05-04) 29 | 30 | #### :bug: Bug Fix 31 | * [#274](https://github.com/DockYard/ember-in-viewport/pull/274) [Bug]: Upgrade intersection-observer-admin ([@snewcomer](https://github.com/snewcomer)) 32 | 33 | #### Committers: 1 34 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 35 | 36 | 37 | ## v3.10.1 (2021-05-03) 38 | 39 | #### :bug: Bug Fix 40 | * [#273](https://github.com/DockYard/ember-in-viewport/pull/273) [Bug]: revert intersection-observer-admin bug with multiple elements sharing a single root ([@snewcomer](https://github.com/snewcomer)) 41 | 42 | #### Committers: 1 43 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 44 | 45 | 46 | ## v3.10.0 (2021-04-27) 47 | 48 | #### :bug: Bug Fix 49 | * [#270](https://github.com/DockYard/ember-in-viewport/pull/270) Bug: stop watching unless viewportSpy=true is passed to modifier ([@snewcomer](https://github.com/snewcomer)) 50 | 51 | #### Committers: 1 52 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 53 | 54 | 55 | ## v3.9.0 (2021-03-22) 56 | 57 | #### :bug: Bug Fix 58 | * [#268](https://github.com/DockYard/ember-in-viewport/pull/268) Bug: Support scrollableArea as an element ([@SergeAstapov](https://github.com/SergeAstapov)) 59 | 60 | #### Committers: 1 61 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 62 | 63 | 64 | ## v3.8.1 (2020-12-10) 65 | 66 | #### :rocket: Enhancement 67 | * [#259](https://github.com/DockYard/ember-in-viewport/pull/259) Upgrade Ember CLI and blueprint to 3.22 ([@SergeAstapov](https://github.com/SergeAstapov)) 68 | * [#250](https://github.com/DockYard/ember-in-viewport/pull/250) Ensure works with custom element as a sentinel ([@snewcomer](https://github.com/snewcomer)) 69 | 70 | #### Committers: 3 71 | - Luke Melia ([@lukemelia](https://github.com/lukemelia)) 72 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 73 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov)) 74 | 75 | 76 | ## v3.7.8 (2020-08-02) 77 | 78 | #### :bug: Bug Fix 79 | * [#248](https://github.com/DockYard/ember-in-viewport/pull/248) [BUG]: Unable to use multiple {{in-viewport}} modifiers ([@snewcomer](https://github.com/snewcomer)) 80 | 81 | #### Committers: 1 82 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 83 | 84 | 85 | ## v3.7.7 (2020-07-28) 86 | 87 | #### :bug: Bug Fix 88 | * [#244](https://github.com/DockYard/ember-in-viewport/pull/244) ScheduleOnce is not deduping ([@snewcomer](https://github.com/snewcomer)) 89 | 90 | #### Committers: 2 91 | - Mehul Kar ([@mehulkar](https://github.com/mehulkar)) 92 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 93 | 94 | 95 | ## v3.7.6 (2020-07-14) 96 | 97 | #### :bug: Bug Fix 98 | * [#241](https://github.com/DockYard/ember-in-viewport/pull/241) Fix test failures in Ember 3.20: Run service teardown code in willDestroy() ([@bendemboski](https://github.com/bendemboski)) 99 | 100 | #### Committers: 1 101 | - Ben Demboski ([@bendemboski](https://github.com/bendemboski)) 102 | 103 | 104 | ## v3.7.5 (2020-06-15) 105 | 106 | #### :bug: Bug Fix 107 | * [#239](https://github.com/DockYard/ember-in-viewport/pull/239) Fixing in-viewport element modifier with rAF-based detection ([@gmurphey](https://github.com/gmurphey)) 108 | 109 | #### Committers: 1 110 | - Garrett Murphey ([@gmurphey](https://github.com/gmurphey)) 111 | 112 | 113 | ## v3.7.4 (2020-06-09) 114 | 115 | #### :rocket: Enhancement 116 | * [#236](https://github.com/DockYard/ember-in-viewport/pull/236) MAJOR: Node 10 ([@snewcomer](https://github.com/snewcomer)) 117 | 118 | #### :bug: Bug Fix 119 | * [#238](https://github.com/DockYard/ember-in-viewport/pull/238) Warning on addToRegistry after destroy ([@snewcomer](https://github.com/snewcomer)) 120 | 121 | #### Committers: 1 122 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 123 | 124 | 125 | ## v3.7.3 (2020-05-07) 126 | 127 | #### :rocket: Enhancement 128 | * [#235](https://github.com/DockYard/ember-in-viewport/pull/235) Resolve async issues with service#destroy and tests ([@snewcomer](https://github.com/snewcomer)) 129 | 130 | #### :bug: Bug Fix 131 | * [#235](https://github.com/DockYard/ember-in-viewport/pull/235) Resolve async issues with service#destroy and tests ([@snewcomer](https://github.com/snewcomer)) 132 | 133 | #### Committers: 1 134 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 135 | 136 | 137 | ## v3.7.2 (2020-03-09) 138 | 139 | #### :rocket: Enhancement 140 | * [#225](https://github.com/DockYard/ember-in-viewport/pull/225) Mixin and viewportDidScroll removal warning in development ([@snewcomer](https://github.com/snewcomer)) 141 | 142 | #### :bug: Bug Fix 143 | * [#229](https://github.com/DockYard/ember-in-viewport/pull/229) Bump IntersectionObserverAdmin to support Cordova ([@snewcomer](https://github.com/snewcomer)) 144 | 145 | #### Committers: 1 146 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 147 | 148 | 149 | ## v3.7.1 (2019-12-30) 150 | 151 | #### :rocket: Enhancement 152 | * [#221](https://github.com/DockYard/ember-in-viewport/pull/221) Bump to latest io-admin 0.2.10 ([@snewcomer](https://github.com/snewcomer)) 153 | * [#219](https://github.com/DockYard/ember-in-viewport/pull/219) Improve docs ([@snewcomer](https://github.com/snewcomer)) 154 | 155 | #### Committers: 1 156 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 157 | 158 | 159 | ## v3.7.0 (2019-12-21) 160 | 161 | #### :rocket: Enhancement 162 | * [#215](https://github.com/DockYard/ember-in-viewport/pull/215) Built in modifier test and README ([@snewcomer](https://github.com/snewcomer)) 163 | 164 | #### Committers: 1 165 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 166 | 167 | 168 | ## v3.6.2 (2019-12-18) 169 | 170 | #### :rocket: Enhancement 171 | * [#214](https://github.com/DockYard/ember-in-viewport/pull/214) 3.12 ([@snewcomer](https://github.com/snewcomer)) 172 | * [#208](https://github.com/DockYard/ember-in-viewport/pull/208) feat(modifiers): add `in-viewport`, `did-enter-viewport`, `did-exit-viewport` ([@buschtoens](https://github.com/buschtoens)) 173 | 174 | #### :bug: Bug Fix 175 | * [#217](https://github.com/DockYard/ember-in-viewport/pull/217) Closes [#216](https://github.com/dockyard/ember-in-viewport/issues/216), remove unnecessary call to startIntersectionObserver ([@dbashford](https://github.com/dbashford)) 176 | 177 | #### Committers: 4 178 | - David Bashford ([@dbashford](https://github.com/dbashford)) 179 | - Jan Buschtöns ([@buschtoens](https://github.com/buschtoens)) 180 | - Lauren Tan ([@poteto](https://github.com/poteto)) 181 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 182 | 183 | 184 | ## v3.6.1 (2019-10-13) 185 | 186 | #### :rocket: Enhancement 187 | * [#212](https://github.com/DockYard/ember-in-viewport/pull/212) Update deps and security vulns ([@snewcomer](https://github.com/snewcomer)) 188 | 189 | #### Committers: 1 190 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 191 | 192 | 193 | ## v3.6.0 (2019-09-25) 194 | 195 | #### :rocket: Enhancement 196 | * [#210](https://github.com/DockYard/ember-in-viewport/pull/210) Support scrollableArea as an element ([@snewcomer](https://github.com/snewcomer)) 197 | 198 | #### Committers: 1 199 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 200 | 201 | 202 | ## v3.5.9 (2019-09-15) 203 | 204 | #### :rocket: Enhancement 205 | * [#206](https://github.com/DockYard/ember-in-viewport/pull/206) Update raf-pool 0.1.2 ([@snewcomer](https://github.com/snewcomer)) 206 | 207 | #### Committers: 2 208 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 209 | - Vasanth ([@vasind](https://github.com/vasind)) 210 | 211 | 212 | ## v3.5.8 (2019-05-24) 213 | 214 | #### :rocket: Enhancement 215 | * [#199](https://github.com/DockYard/ember-in-viewport/pull/199) Update IO 0.2.4 with simpler replacer func ([@snewcomer](https://github.com/snewcomer)) 216 | 217 | #### :bug: Bug Fix 218 | * [#199](https://github.com/DockYard/ember-in-viewport/pull/199) Update IO 0.2.4 with simpler replacer func ([@snewcomer](https://github.com/snewcomer)) 219 | 220 | #### Committers: 1 221 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 222 | 223 | 224 | ## v3.5.7 (2019-05-24) 225 | 226 | #### :bug: Bug Fix 227 | * [#198](https://github.com/DockYard/ember-in-viewport/pull/198) Fix stringify replacer function ([@snewcomer](https://github.com/snewcomer)) 228 | 229 | #### Committers: 1 230 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 231 | 232 | 233 | ## v3.5.6 (2019-05-23) 234 | 235 | #### :rocket: Enhancement 236 | * [#197](https://github.com/DockYard/ember-in-viewport/pull/197) Bump IO library to 0.2.2 ([@snewcomer](https://github.com/snewcomer)) 237 | 238 | #### :bug: Bug Fix 239 | * [#196](https://github.com/DockYard/ember-in-viewport/pull/196) Fix didScroll ([@snewcomer](https://github.com/snewcomer)) 240 | 241 | #### Committers: 1 242 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 243 | 244 | 245 | ## v3.5.5 (2019-05-14) 246 | 247 | #### :bug: Bug Fix 248 | * [#194](https://github.com/DockYard/ember-in-viewport/pull/194) Protect if no admin instance ([@snewcomer](https://github.com/snewcomer)) 249 | 250 | #### Committers: 1 251 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 252 | 253 | 254 | ## v3.5.4 (2019-05-08) 255 | 256 | #### :rocket: Enhancement 257 | * [#193](https://github.com/DockYard/ember-in-viewport/pull/193) Pass IO Entry in callback ([@snewcomer](https://github.com/snewcomer)) 258 | 259 | #### Committers: 1 260 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 261 | 262 | 263 | ## v3.5.3 (2019-05-02) 264 | 265 | #### :bug: Bug Fix 266 | * [#192](https://github.com/DockYard/ember-in-viewport/pull/192) Dont start rAF if has IntersectionObserver ([@snewcomer](https://github.com/snewcomer)) 267 | 268 | #### Committers: 1 269 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 270 | 271 | 272 | ## v3.5.2 (2019-04-27) 273 | 274 | #### :rocket: Enhancement 275 | * [#190](https://github.com/DockYard/ember-in-viewport/pull/190) rAF: small refactor to use new recursive function ([@snewcomer](https://github.com/snewcomer)) 276 | * [#185](https://github.com/DockYard/ember-in-viewport/pull/185) New API: watchElement for classes with no mixin ([@snewcomer](https://github.com/snewcomer)) 277 | 278 | #### :bug: Bug Fix 279 | * [#188](https://github.com/DockYard/ember-in-viewport/pull/188) Forgot removeRAF in recursive fn ([@snewcomer](https://github.com/snewcomer)) 280 | 281 | #### Committers: 1 282 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 283 | 284 | 285 | ## v3.5.0 (2019-04-20) 286 | 287 | #### :rocket: Enhancement 288 | * [#184](https://github.com/DockYard/ember-in-viewport/pull/184) Internal: Update IntersectionObserverAdmin + simplify ([@snewcomer](https://github.com/snewcomer)) 289 | 290 | #### Committers: 1 291 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 292 | 293 | 294 | ## v3.4.0 (2019-04-18) 295 | 296 | #### :rocket: Enhancement 297 | * [#182](https://github.com/DockYard/ember-in-viewport/pull/182) Add API to not listen to scroll direction ([@snewcomer](https://github.com/snewcomer)) 298 | 299 | #### Committers: 1 300 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 301 | 302 | 303 | ## v3.3.0 (2019-03-25) 304 | 305 | #### :rocket: Enhancement 306 | * [#179](https://github.com/DockYard/ember-in-viewport/pull/179) Just a small town boy living in a ember modifier world ([@snewcomer](https://github.com/snewcomer)) 307 | * [#178](https://github.com/DockYard/ember-in-viewport/pull/178) Update 3.8 and prep for modifiers ([@snewcomer](https://github.com/snewcomer)) 308 | 309 | #### Committers: 1 310 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 311 | 312 | 313 | ## v3.2.2 (2018-12-11) 314 | 315 | #### :rocket: Enhancement 316 | * [#175](https://github.com/DockYard/ember-in-viewport/pull/175) Ensure rAF takes into account sentinels w and h ([@snewcomer](https://github.com/snewcomer)) 317 | * [#174](https://github.com/DockYard/ember-in-viewport/pull/174) Ensure dummy app runs in ie11 ([@snewcomer](https://github.com/snewcomer)) 318 | 319 | #### :bug: Bug Fix 320 | * [#175](https://github.com/DockYard/ember-in-viewport/pull/175) Ensure rAF takes into account sentinels w and h ([@snewcomer](https://github.com/snewcomer)) 321 | 322 | #### Committers: 1 323 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 324 | 325 | 326 | ## v3.2.1 (2018-12-02) 327 | 328 | #### :rocket: Enhancement 329 | * [#172](https://github.com/DockYard/ember-in-viewport/pull/172) Remove deprecated travis sudo:false flag ([@snewcomer](https://github.com/snewcomer)) 330 | 331 | #### :bug: Bug Fix 332 | * [#173](https://github.com/DockYard/ember-in-viewport/pull/173) IE11 fix ([@snewcomer](https://github.com/snewcomer)) 333 | 334 | #### Committers: 1 335 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 336 | 337 | 338 | ## v3.2.0 (2018-11-27) 339 | 340 | #### :rocket: Enhancement 341 | * [#170](https://github.com/DockYard/ember-in-viewport/pull/170) Use rAFPool npm pkg ([@snewcomer](https://github.com/snewcomer)) 342 | 343 | #### Committers: 1 344 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 345 | 346 | 347 | ## 3.2.0 (2018-11-27) 348 | 349 | #### :rocket: Enhancement 350 | * [#170](https://github.com/DockYard/ember-in-viewport/pull/170) Use rAFPool npm pkg ([@snewcomer](https://github.com/snewcomer)) 351 | 352 | #### Committers: 1 353 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 354 | 355 | 356 | ## v3.1.5 (2018-10-25) 357 | 358 | #### :bug: Bug Fix 359 | * [#168](https://github.com/DockYard/ember-in-viewport/pull/168) Fix scope memory leak ([@snewcomer](https://github.com/snewcomer)) 360 | 361 | #### Committers: 1 362 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 363 | 364 | 365 | ## v3.1.4 (2018-10-11) 366 | 367 | #### :rocket: Enhancement 368 | * [#127](https://github.com/DockYard/ember-in-viewport/pull/127) right left scrolling example ([@snewcomer](https://github.com/snewcomer)) 369 | * [#163](https://github.com/DockYard/ember-in-viewport/pull/163) Update to 3.4 and resolve audit warning ([@snewcomer](https://github.com/snewcomer)) 370 | 371 | #### Committers: 3 372 | - Ben Limmer ([@blimmer](https://github.com/blimmer)) 373 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 374 | - sir.dinha ([@sardyy](https://github.com/sardyy)) 375 | 376 | 377 | ## v3.1.3 (2018-08-27) 378 | 379 | #### :rocket: Enhancement 380 | * [#162](https://github.com/DockYard/ember-in-viewport/pull/162) Handle root with gaining sizzle properties ([@snewcomer](https://github.com/snewcomer)) 381 | 382 | #### :bug: Bug Fix 383 | * [#162](https://github.com/DockYard/ember-in-viewport/pull/162) Handle root with gaining sizzle properties ([@snewcomer](https://github.com/snewcomer)) 384 | 385 | #### Committers: 1 386 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 387 | 388 | 389 | ## v3.1.2 (2018-08-23) 390 | 391 | #### :rocket: Enhancement 392 | * [#158](https://github.com/DockYard/ember-in-viewport/pull/158) Update deps to fix security audit ([@snewcomer](https://github.com/snewcomer)) 393 | 394 | #### :bug: Bug Fix 395 | * [#159](https://github.com/DockYard/ember-in-viewport/pull/159) Do not cancel rAF as this effects other elements that are observed ([@snewcomer](https://github.com/snewcomer)) 396 | 397 | #### Committers: 1 398 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 399 | 400 | 401 | ## 3.1.2 (2018-08-23) 402 | 403 | #### :rocket: Enhancement 404 | * [#158](https://github.com/DockYard/ember-in-viewport/pull/158) Update deps to fix security audit ([@snewcomer](https://github.com/snewcomer)) 405 | 406 | #### :bug: Bug Fix 407 | * [#159](https://github.com/DockYard/ember-in-viewport/pull/159) Do not cancel rAF as this effects other elements that are observed ([@snewcomer](https://github.com/snewcomer)) 408 | 409 | #### Committers: 1 410 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 411 | 412 | 413 | ## v3.1.1 (2018-08-21) 414 | 415 | #### :rocket: Enhancement 416 | * [#157](https://github.com/DockYard/ember-in-viewport/pull/157) Allow `root` on static admin to have multiple keys ([@snewcomer](https://github.com/snewcomer)) 417 | 418 | #### Committers: 1 419 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 420 | 421 | 422 | ## v3.1.0 (2018-07-27) 423 | 424 | #### :rocket: Enhancement 425 | * [#153](https://github.com/DockYard/ember-in-viewport/pull/153) Use one IntersectionObserver per viewport ([@snewcomer](https://github.com/snewcomer)) 426 | * [#152](https://github.com/DockYard/ember-in-viewport/pull/152) Stop rAF loop if transition from page that uses rAF to page that uses IO ([@snewcomer](https://github.com/snewcomer)) 427 | 428 | #### Committers: 1 429 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 430 | 431 | 432 | ## v3.0.4 (2018-07-13) 433 | 434 | #### :rocket: Enhancement 435 | * [#150](https://github.com/DockYard/ember-in-viewport/pull/150) updates + fix w/ npm audit ([@snewcomer](https://github.com/snewcomer)) 436 | * [#147](https://github.com/DockYard/ember-in-viewport/pull/147) Ensure explicitly setting viewportUseIntersectionObserver is not allowed ([@snewcomer](https://github.com/snewcomer)) 437 | * [#145](https://github.com/DockYard/ember-in-viewport/pull/145) use one rAF manager ([@snewcomer](https://github.com/snewcomer)) 438 | * [#144](https://github.com/DockYard/ember-in-viewport/pull/144) Cancel animation frame before requesting again ([@jheth](https://github.com/jheth)) 439 | * [#143](https://github.com/DockYard/ember-in-viewport/pull/143) add note about didExitViewport and intersectionThreshold ([@snewcomer](https://github.com/snewcomer)) 440 | 441 | #### :bug: Bug Fix 442 | * [#147](https://github.com/DockYard/ember-in-viewport/pull/147) Ensure explicitly setting viewportUseIntersectionObserver is not allowed ([@snewcomer](https://github.com/snewcomer)) 443 | 444 | #### Committers: 2 445 | - Joe Heth ([@jheth](https://github.com/jheth)) 446 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 447 | 448 | 449 | ## 3.0.4 (2018-07-13) 450 | 451 | #### :rocket: Enhancement 452 | * [#150](https://github.com/DockYard/ember-in-viewport/pull/150) updates + fix w/ npm audit ([@snewcomer](https://github.com/snewcomer)) 453 | * [#147](https://github.com/DockYard/ember-in-viewport/pull/147) Ensure explicitly setting viewportUseIntersectionObserver is not allowed ([@snewcomer](https://github.com/snewcomer)) 454 | * [#145](https://github.com/DockYard/ember-in-viewport/pull/145) use one rAF manager ([@snewcomer](https://github.com/snewcomer)) 455 | * [#144](https://github.com/DockYard/ember-in-viewport/pull/144) Cancel animation frame before requesting again ([@jheth](https://github.com/jheth)) 456 | * [#143](https://github.com/DockYard/ember-in-viewport/pull/143) add note about didExitViewport and intersectionThreshold ([@snewcomer](https://github.com/snewcomer)) 457 | 458 | #### :bug: Bug Fix 459 | * [#147](https://github.com/DockYard/ember-in-viewport/pull/147) Ensure explicitly setting viewportUseIntersectionObserver is not allowed ([@snewcomer](https://github.com/snewcomer)) 460 | 461 | #### Committers: 2 462 | - Joe Heth ([@jheth](https://github.com/jheth)) 463 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 464 | 465 | 466 | ## v3.0.2 (2018-04-15) 467 | 468 | #### :rocket: Enhancement 469 | * [#140](https://github.com/DockYard/ember-in-viewport/pull/140) upgrade to 3.1 ember ([@snewcomer](https://github.com/snewcomer)) 470 | 471 | #### Committers: 1 472 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 473 | 474 | 475 | ## 3.0.2 (2018-04-15) 476 | 477 | #### :rocket: Enhancement 478 | * [#140](https://github.com/DockYard/ember-in-viewport/pull/140) upgrade to 3.1 ember ([@snewcomer](https://github.com/snewcomer)) 479 | 480 | #### Committers: 1 481 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 482 | 483 | 484 | ## v3.0.1 (2018-04-06) 485 | 486 | #### :rocket: Enhancement 487 | * [#138](https://github.com/DockYard/ember-in-viewport/pull/138) Bugfix - send action on destroy + memory leaks ([@snewcomer](https://github.com/snewcomer)) 488 | * [#134](https://github.com/DockYard/ember-in-viewport/pull/134) add firefox to travis ([@snewcomer](https://github.com/snewcomer)) 489 | 490 | #### :bug: Bug Fix 491 | * [#138](https://github.com/DockYard/ember-in-viewport/pull/138) Bugfix - send action on destroy + memory leaks ([@snewcomer](https://github.com/snewcomer)) 492 | 493 | #### Committers: 2 494 | - Marten ([@martndemus](https://github.com/martndemus)) 495 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 496 | 497 | 498 | ## 3.0.1 (2018-04-06) 499 | 500 | #### :rocket: Enhancement 501 | * [#138](https://github.com/DockYard/ember-in-viewport/pull/138) Bugfix - send action on destroy + memory leaks ([@snewcomer](https://github.com/snewcomer)) 502 | * [#134](https://github.com/DockYard/ember-in-viewport/pull/134) add firefox to travis ([@snewcomer](https://github.com/snewcomer)) 503 | 504 | #### :bug: Bug Fix 505 | * [#138](https://github.com/DockYard/ember-in-viewport/pull/138) Bugfix - send action on destroy + memory leaks ([@snewcomer](https://github.com/snewcomer)) 506 | 507 | #### Committers: 2 508 | - Marten ([@martndemus](https://github.com/martndemus)) 509 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 510 | 511 | 512 | ## v3.0.0 (2018-03-02) 513 | 514 | #### :rocket: Enhancement 515 | * [#133](https://github.com/DockYard/ember-in-viewport/pull/133) Raf test bounding ([@snewcomer](https://github.com/snewcomer)) 516 | * [#130](https://github.com/DockYard/ember-in-viewport/pull/130) Improve docs ([@snewcomer](https://github.com/snewcomer)) 517 | * [#121](https://github.com/DockYard/ember-in-viewport/pull/121) Upgrade to 3.0 and remove jquery usage ([@snewcomer](https://github.com/snewcomer)) 518 | 519 | #### :bug: Bug Fix 520 | * [#132](https://github.com/DockYard/ember-in-viewport/pull/132) update rafLogic for scrollable area ([@snewcomer](https://github.com/snewcomer)) 521 | 522 | #### Committers: 1 523 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 524 | 525 | 526 | ## 3.0.0 (2018-03-02) 527 | 528 | #### :rocket: Enhancement 529 | * [#133](https://github.com/DockYard/ember-in-viewport/pull/133) Raf test bounding ([@snewcomer](https://github.com/snewcomer)) 530 | * [#130](https://github.com/DockYard/ember-in-viewport/pull/130) Improve docs ([@snewcomer](https://github.com/snewcomer)) 531 | * [#121](https://github.com/DockYard/ember-in-viewport/pull/121) Upgrade to 3.0 and remove jquery usage ([@snewcomer](https://github.com/snewcomer)) 532 | 533 | #### :bug: Bug Fix 534 | * [#132](https://github.com/DockYard/ember-in-viewport/pull/132) update rafLogic for scrollable area ([@snewcomer](https://github.com/snewcomer)) 535 | 536 | #### Committers: 1 537 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 538 | 539 | 540 | ## v2.2.1 (2018-02-23) 541 | 542 | #### :rocket: Enhancement 543 | * [#126](https://github.com/DockYard/ember-in-viewport/pull/126) change intersection threshold default to 0 ([@snewcomer](https://github.com/snewcomer)) 544 | * [#125](https://github.com/DockYard/ember-in-viewport/pull/125) Move viewport config to addon folder Closes [#124](https://github.com/dockyard/ember-in-viewport/issues/124) ([@snewcomer](https://github.com/snewcomer)) 545 | 546 | #### :bug: Bug Fix 547 | * [#125](https://github.com/DockYard/ember-in-viewport/pull/125) Move viewport config to addon folder Closes [#124](https://github.com/dockyard/ember-in-viewport/issues/124) ([@snewcomer](https://github.com/snewcomer)) 548 | 549 | #### Committers: 1 550 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 551 | 552 | 553 | ## 2.2.1 (2018-02-23) 554 | 555 | #### :rocket: Enhancement 556 | * [#126](https://github.com/DockYard/ember-in-viewport/pull/126) change intersection threshold default to 0 ([@snewcomer](https://github.com/snewcomer)) 557 | * [#125](https://github.com/DockYard/ember-in-viewport/pull/125) Move viewport config to addon folder Closes [#124](https://github.com/dockyard/ember-in-viewport/issues/124) ([@snewcomer](https://github.com/snewcomer)) 558 | 559 | #### :bug: Bug Fix 560 | * [#125](https://github.com/DockYard/ember-in-viewport/pull/125) Move viewport config to addon folder Closes [#124](https://github.com/dockyard/ember-in-viewport/issues/124) ([@snewcomer](https://github.com/snewcomer)) 561 | 562 | #### Committers: 1 563 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 564 | 565 | 566 | ## v2.2.0 (2018-02-08) 567 | 568 | #### :rocket: Enhancement 569 | * [#113](https://github.com/DockYard/ember-in-viewport/pull/113) Use IntersectionObserver if available ([@snewcomer](https://github.com/snewcomer)) 570 | 571 | #### :bug: Bug Fix 572 | * [#122](https://github.com/DockYard/ember-in-viewport/pull/122) Fix viewportTolerance with defaults ([@snewcomer](https://github.com/snewcomer)) 573 | 574 | #### Committers: 5 575 | - Alexander Lang ([@langalex](https://github.com/langalex)) 576 | - Alvin Crespo ([@alvincrespo](https://github.com/alvincrespo)) 577 | - Miguel Camba ([@cibernox](https://github.com/cibernox)) 578 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 579 | - [@hybridmuse](https://github.com/hybridmuse) 580 | 581 | 582 | ## 2.2.0 (2018-02-08) 583 | 584 | #### :rocket: Enhancement 585 | * [#113](https://github.com/DockYard/ember-in-viewport/pull/113) Use IntersectionObserver if available ([@snewcomer](https://github.com/snewcomer)) 586 | 587 | #### :bug: Bug Fix 588 | * [#122](https://github.com/DockYard/ember-in-viewport/pull/122) Fix viewportTolerance with defaults ([@snewcomer](https://github.com/snewcomer)) 589 | 590 | #### Committers: 5 591 | - Alexander Lang ([@langalex](https://github.com/langalex)) 592 | - Alvin Crespo ([@alvincrespo](https://github.com/alvincrespo)) 593 | - Miguel Camba ([@cibernox](https://github.com/cibernox)) 594 | - Scott Newcomer ([@snewcomer](https://github.com/snewcomer)) 595 | - [@hybridmuse](https://github.com/hybridmuse) 596 | --------------------------------------------------------------------------------