Skip to content

Commit

Permalink
Merge branch 'release/1.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
poteto committed Apr 27, 2015
2 parents 91dda56 + 8f2e00f commit 2c894e6
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 79 deletions.
90 changes: 52 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

[![npm version](https://badge.fury.io/js/ember-in-viewport.svg)](http://badge.fury.io/js/ember-in-viewport) [![Build Status](https://travis-ci.org/dockyard/ember-in-viewport.svg)](https://travis-ci.org/dockyard/ember-in-viewport)

This `ember-cli` addon adds a simple, highly performant Ember Mixin to your app. This mixin, when added to a `View` or `Component` (collectively referred to as `Components`), will allow you to check if that `Component` has entered the browser's viewport. By default, the Mixin uses the `requestAnimationFrame` API if it detects it in your user's browser – failing which, it fallsback to using the more resource heavy Ember run loop and event listeners.
This `ember-cli` addon adds a simple, highly performant Ember Mixin to your app. This Mixin, when added to a `View` or `Component` (collectively referred to as `Components`), will allow you to check if that `Component` has entered the browser's viewport. By default, the Mixin uses the `requestAnimationFrame` API if it detects it in your user's browser – failing which, it fallsback to using the Ember run loop and event listeners.

## Demo
- App: http://development.ember-in-viewport-demo.divshot.io/
- Source: https://github.com/poteto/ember-in-viewport-demo

## Usage
Usage is simple. First, add the mixin to your `Component`:
Usage is simple. First, add the Mixin to your `Component`:

```js
import Ember from 'ember';
Expand All @@ -30,26 +30,6 @@ These hooks fire once whenever the `Component` enters or exits the viewport. You

```js
export default Ember.Component.extend(InViewportMixin, {

// with prototype extensions disabled
handleDidEnterViewport: Ember.on('didEnterViewport', function() {
console.log('entered');
}),

handleDidExitViewport: Ember.on('didExitViewport', function() {
console.log('exited');
}),

// with prototype extensions enabled
handleDidEnterViewport: (function() {
console.log('entered');
}).on('didEnterViewport'),

handleDidExitViewport: (function() {
console.log('exited');
}).on('didExitViewport'),

// method override
didEnterViewport() {
console.log('entered');
},
Expand All @@ -60,25 +40,25 @@ export default Ember.Component.extend(InViewportMixin, {
});
```

##### [BETA] `didScroll{Up,Down,Left,Right}`
The appropriate scroll hook fires when an element enters the viewport. For example, if you scrolled down in order to move the element in the viewport, the `didScrollDown` hook would fire. You can then handle it like another hook as in the above example.
##### `didScroll{Up,Down,Left,Right}`
The appropriate scroll hook fires when an element enters the viewport. For example, if you scrolled down in order to move the element in the viewport, the `didScrollDown` hook would fire. You can then handle it like another hook as in the above example. Optionally, you can also receive the direction as a string by passing a single argument to the hook.

```js
export default Ember.Component.extend(InViewportMixin, {
didScrollUp() {
console.log('up');
didScrollUp(direction) {
console.log(direction); // 'up'
},

didScrollDown() {
console.log('down');
didScrollDown(direction) {
console.log(direction); // 'down'
},

didScrollLeft() {
console.log('left');
didScrollLeft(direction) {
console.log(direction); // 'left'
},

didScrollRight() {
console.log('right');
didScrollRight(direction) {
console.log(direction); // 'right'
}
});
```
Expand All @@ -102,9 +82,10 @@ The mixin comes with some options. Due to the way listeners and `requestAnimatio
export default Ember.Component.extend(InViewportMixin, {
viewportOptionsOverride: Ember.on('didInsertElement', function() {
Ember.setProperties(this, {
viewportUseRAF : true,
viewportSpy : false,
viewportRefreshRate : 150,
viewportUseRAF : true,
viewportSpy : false,
viewportScrollSensitivity : 1,
viewportRefreshRate : 150,
viewportTolerance: {
top : 50,
bottom : 50,
Expand All @@ -120,26 +101,59 @@ export default Ember.Component.extend(InViewportMixin, {

Default: Depends on browser

As it's name suggests, if this is `true`, the mixin will use `requestAnimationFrame` instead of the Ember run loop. Unless you want to force enabling or disabling this, you won't need to override this option.
As it's name suggests, if this is `true`, the Mixin will use `requestAnimationFrame` instead of the Ember run loop. Unless you want to force enabling or disabling this, you won't need to override this option.

- `viewportSpy: boolean`

Default: `false`

When `true`, the mixin 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 mixin will only watch the `Component` until it enters the viewport once, and then it sets `viewportEntered` to `true` (permanently), and unbinds listeners. This reduces the load on the Ember run loop and your application.
When `true`, the Mixin 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 Mixin will only watch the `Component` until it enters the viewport once, and then it sets `viewportEntered` to `true` (permanently), and unbinds listeners. This reduces the load on the Ember run loop and your application.

- `viewportScrollSensitivity: number`

Default: `1`

This value determines the degree of sensitivity (in `px`) in which a DOM element is considered to have scrolled into the viewport. For example, if you set `viewportScrollSensitivity` to `10`, the `didScroll{...}` hooks would only fire if the scroll was greater than `10px`.

- `viewportRefreshRate: number`

Default: `100`

If `requestAnimationFrame` is not present, this value determines how often the mixin checks your component to determine whether or not it has entered or left the viewport. The lower this number, the more often it checks, and the more load is placed on your application. Generally, you'll want this value between `100` to `300`, which is about the range at which people consider things to be "real-time".
If `requestAnimationFrame` is not present, this value determines how often the Mixin checks your component to determine whether or not it has entered or left the viewport. The lower this number, the more often it checks, and the more load is placed on your application. Generally, you'll want this value between `100` to `300`, which is about the range at which people consider things to be "real-time".

This value also affects how often the Mixin checks scroll direction.

- `viewportTolerance: object`

Default: `{ top: 0, left: 0, bottom: 0, right: 0 }`

This option determines how accurately the `Component` needs to be within the viewport for it to be considered as entered.

### Global options

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:

```js
module.exports = function(environment) {
var ENV = {
// ...
viewportConfig: {
viewportSpy : false,
viewportUseRAF : true,
viewportScrollSensitivity : 1,
viewportRefreshRate : 100,
viewportListeners : [],
viewportTolerance: {
top : 0,
left : 0,
bottom : 0,
right : 0
}
}
};
};
```

## Installation

* `git clone` this repository
Expand Down
86 changes: 47 additions & 39 deletions addon/mixins/in-viewport.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ const {
set: set,
setProperties,
computed,
merge,
typeOf,
run,
on,
$,
$
} = Ember;

const {
Expand All @@ -25,41 +27,43 @@ const { not } = computed;
const { forEach } = Ember.EnumerableUtils;
const { classify } = Ember.String;

const listeners = [
const defaultListeners = [
{ context: window, event: 'scroll.scrollable' },
{ context: window, event: 'resize.resizable' },
{ context: document, event: 'touchmove.scrollable' }
];

let rAFIDS = {};
let lastDirection = {};
let lastPosition = {};
const rAFIDS = {};
const lastDirection = {};
const lastPosition = {};

export default Ember.Mixin.create({
viewportExited: not('viewportEntered').readOnly(),

_setInitialState: on('init', function() {
setProperties(this, {
viewportUseRAF : canUseRAF(),
viewportEntered : false,
viewportSpy : false,
viewportRefreshRate : 100,
viewportTolerance : {
top : 0,
left : 0,
bottom : 0,
right : 0
},
scrollSensitivity: 1
});
const options = merge({
viewportUseRAF : canUseRAF(),
viewportEntered : false,
viewportListeners : defaultListeners
}, this._buildOptions());

setProperties(this, options);
}),

_buildOptions(defaultOptions = []) {
if (this.container) {
return merge(defaultOptions, this.container.lookup('config:in-viewport'));
}
},

_setupElement: on('didInsertElement', function() {
if (!canUseDOM) { return; }

this._setInitialViewport(window);
this._addObserverIfNotSpying();
this._bindScrollDirectionListener(window, get(this, 'scrollSensitivity'));
this._bindScrollDirectionListener(window, get(this, 'viewportScrollSensitivity'));

const listeners = get(this, 'viewportListeners');

if (!get(this, 'viewportUseRAF')) {
forEach(listeners, (listener) => {
Expand All @@ -82,15 +86,19 @@ export default Ember.Mixin.create({
_setViewportEntered(context = null) {
Ember.assert('You must pass a valid context to _setViewportEntered', context);

const element = get(this, 'element');

if (!element) { return; }

const elementId = get(this, 'elementId');
const viewportUseRAF = get(this, 'viewportUseRAF');
const viewportTolerance = get(this, 'viewportTolerance');
const elementId = get(this, 'elementId');
const boundingClientRect = get(this, 'element').getBoundingClientRect();
const $contextEl = $(context);
const height = $contextEl.height();
const width = $contextEl.width();
const boundingClientRect = element.getBoundingClientRect();

this._triggerDidEnterViewport(
this._triggerDidAccessViewport(
isInViewport(boundingClientRect, height, width, viewportTolerance)
);

Expand All @@ -114,21 +122,18 @@ export default Ember.Mixin.create({
left : $contextEl.scrollLeft()
};

const scrollDirection = checkScrollDirection(lastPositionForEl, newPosition, sensitivity);
const hasDirection = scrollDirection !== lastDirectionForEl;
const scrollDirection = checkScrollDirection(lastPositionForEl, newPosition, sensitivity);
const directionChanged = scrollDirection !== lastDirectionForEl;

if (hasDirection && viewportEntered) {
this.trigger(`didScroll${classify(scrollDirection)}`);
}

if (viewportEntered) {
if (scrollDirection && directionChanged && viewportEntered) {
this.trigger(`didScroll${classify(scrollDirection)}`, scrollDirection);
lastDirection[elementId] = scrollDirection;
}

lastPosition[elementId] = newPosition;
lastPosition[elementId] = newPosition;
},

_triggerDidEnterViewport(hasEnteredViewport = false) {
_triggerDidAccessViewport(hasEnteredViewport = false) {
const viewportEntered = get(this, 'viewportEntered');
const didEnter = !viewportEntered && hasEnteredViewport;
const didLeave = viewportEntered && !hasEnteredViewport;
Expand Down Expand Up @@ -158,11 +163,13 @@ export default Ember.Mixin.create({
});
},

_scrollHandler(context = null) {
Ember.assert('You must pass a valid context to _scrollHandler', context);
_debouncedEventHandler(methodName, ...args) {
Ember.assert('You must pass a methodName to _debouncedEventHandler', methodName);
const validMethodString = typeOf(methodName) === 'string';
Ember.assert('methodName must be a string', validMethodString);

debounce(this, () => {
this._setViewportEntered(context);
this[methodName](...args);
}, get(this, 'viewportRefreshRate'));
},

Expand All @@ -173,7 +180,7 @@ export default Ember.Mixin.create({
const $contextEl = $(context);

$contextEl.on(`scroll.directional#${get(this, 'elementId')}`, () => {
this._triggerDidScrollDirection($contextEl, sensitivity);
this._debouncedEventHandler('_triggerDidScrollDirection', $contextEl, sensitivity);
});
},

Expand All @@ -183,26 +190,27 @@ export default Ember.Mixin.create({
const elementId = get(this, 'elementId');

$(context).off(`scroll.directional#${elementId}`);
lastPosition[elementId] = null;
lastDirection[elementId] = null;
delete lastPosition[elementId];
delete lastDirection[elementId];
},

_bindListeners(context = null, event = null) {
Ember.assert('You must pass a valid context to _bindListeners', context);
Ember.assert('You must pass a valid event to _bindListeners', event);

$(context).on(`${event}#${get(this, 'elementId')}`, () => {
this._scrollHandler(context);
this._debouncedEventHandler('_setViewportEntered', context);
});
},

_unbindListeners() {
const elementId = get(this, 'elementId');
const listeners = get(this, 'viewportListeners');

if (get(this, 'viewportUseRAF')) {
next(this, () => {
window.cancelAnimationFrame(rAFIDS[elementId]);
rAFIDS[elementId] = null;
delete rAFIDS[elementId];
});
}

Expand Down
12 changes: 12 additions & 0 deletions app/initializers/viewport-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import config from '../config/environment';

export function initialize(_container, application) {
const { viewportConfig } = config;

application.register('config:in-viewport', viewportConfig, { instantiate: false });
}

export default {
name: 'viewport-config',
initialize: initialize
};
17 changes: 16 additions & 1 deletion config/environment.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
/* jshint node: true */

'use strict';

module.exports = function(/* environment, appConfig */) {
return { };
return {
viewportConfig: {
viewportSpy : false,
viewportScrollSensitivity : 1,
viewportRefreshRate : 100,
viewportListeners : [],
viewportTolerance: {
top : 0,
left : 0,
bottom : 0,
right : 0
},
}
};
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ember-in-viewport",
"version": "1.2.0-beta1",
"version": "1.2.0",
"description": "Detect if an Ember View or Component is in the viewport @ 60FPS",
"directories": {
"doc": "doc",
Expand Down
Loading

0 comments on commit 2c894e6

Please sign in to comment.