(function() {
'use strict';
npm install angular-inview@beta
(function() {
'use strict';
An angular.js directive to evaluate an expression if a DOM element is or not in the current visible browser viewport. Use it in your AngularJS app by including the javascript and requireing it:
angular.module('myApp', ['angular-inview'])
var moduleName = 'angular-inview';
angular.module(moduleName, [])
.directive('inView', ['$parse', inViewDirective])
.directive('inViewContainer', inViewContainerDirective);
function inViewDirective ($parse) {
return {
Evaluate the expression passet to the attribute in-view
when the DOM
element is visible in the viewport.
restrict: 'A',
require: '?^^inViewContainer',
link: function inViewDirectiveLink (scope, element, attrs, container) {
in-view-options attribute can be specified with an object expression containing:
offset
: An array of values to offset the element position.
Offsets are expressed as arrays of 4 numbers [top, right, bottom, left].
Like CSS, you can also specify only 2 numbers [top/bottom, left/right].
Instead of numbers, some array elements can be a string with a percentage.
Positive numbers are offsets outside the element rectangle and
negative numbers are offsets to the inside.viewportOffset
: Like the element offset but appied to the viewport.generateDirection
: Indicate if the direction
information should
be included in $inviewInfo
(default false).generateParts
: Indicate if the parts
information should
be included in $inviewInfo
(default false).throttle
: Specify a number of milliseconds by which to limit the
number of incoming events. var options = {};
if (attrs.inViewOptions) {
options = scope.$eval(attrs.inViewOptions);
}
if (options.offset) {
options.offset = normalizeOffset(options.offset);
}
if (options.viewportOffset) {
options.viewportOffset = normalizeOffset(options.viewportOffset);
}
Build reactive chain from an initial event
var viewportEventSignal = signalSingle({ type: 'initial' })
Merged with the window events
.merge(signalFromEvent(window, 'checkInView click ready wheel mousewheel DomMouseScroll MozMousePixelScroll resize scroll touchmove mouseup keydown'))
Merge with container’s events signal
if (container) {
viewportEventSignal = viewportEventSignal.merge(container.eventsSignal);
}
Throttle if option specified
if (options.throttle) {
viewportEventSignal = viewportEventSignal.throttle(options.throttle);
}
Map to viewport intersection and in-view informations
var inviewInfoSignal = viewportEventSignal
Inview information structure contains:
inView
: a boolean value indicating if the element is
visible in the viewport;changed
: a boolean value indicating if the inview status
changed after the last event;event
: the event that initiated the in-view check; .map(function(event) {
var viewportRect;
if (container) {
viewportRect = container.getViewportRect();
TODO merge with actual window!
} else {
viewportRect = getViewportRect();
}
viewportRect = offsetRect(viewportRect, options.viewportOffset);
var elementRect = offsetRect(element[0].getBoundingClientRect(), options.offset);
var info = {
inView: intersectRect(elementRect, viewportRect),
event: event,
element: element,
elementRect: elementRect,
viewportRect: viewportRect
};
Add inview parts
if (options.generateParts && info.inView) {
info.parts = {};
info.parts.top = elementRect.top >= viewportRect.top;
info.parts.left = elementRect.left >= viewportRect.left;
info.parts.bottom = elementRect.bottom <= viewportRect.bottom;
info.parts.right = elementRect.right <= viewportRect.right;
}
return info;
})
Add the changed information to the inview structure.
.scan({}, function (lastInfo, newInfo) {
Add inview direction info
if (options.generateDirection && newInfo.inView && lastInfo.elementRect) {
newInfo.direction = {
horizontal: newInfo.elementRect.left - lastInfo.elementRect.left,
vertical: newInfo.elementRect.top - lastInfo.elementRect.top
};
}
Calculate changed flag
newInfo.changed =
newInfo.inView !== lastInfo.inView ||
!angular.equals(newInfo.parts, lastInfo.parts) ||
!angular.equals(newInfo.direction, lastInfo.direction);
return newInfo;
})
Filters only informations that should be forwarded to the callback
.filter(function (info) {
Don’t forward if no relevant infomation changed
if (!info.changed) {
return false;
}
Don’t forward if not initially in-view
if (info.event.type === 'initial' && !info.inView) {
return false;
}
return true;
});
Execute in-view callback
var inViewExpression = $parse(attrs.inView);
var dispose = inviewInfoSignal.subscribe(function (info) {
scope.$applyAsync(function () {
inViewExpression(scope, {
'$inview': info.inView,
'$inviewInfo': info
});
});
});
Dispose of reactive chain
scope.$on('$destroy', dispose);
}
}
}
function inViewContainerDirective () {
return {
restrict: 'A',
controller: ['$element', function ($element) {
this.element = $element;
this.eventsSignal = signalFromEvent($element, 'scroll');
this.getViewportRect = function () {
return $element[0].getBoundingClientRect();
};
}]
}
}
function getViewportRect () {
var result = {
top: 0,
left: 0,
width: window.innerWidth,
right: window.innerWidth,
height: window.innerHeight,
bottom: window.innerHeight
};
if (result.height) {
return result;
}
var mode = document.compatMode;
if (mode === 'CSS1Compat') {
result.width = result.right = document.documentElement.clientWidth;
result.height = result.bottom = document.documentElement.clientHeight;
} else {
result.width = result.right = document.body.clientWidth;
result.height = result.bottom = document.body.clientHeight;
}
return result;
}
function intersectRect (r1, r2) {
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
function normalizeOffset (offset) {
if (!angular.isArray(offset)) {
return [offset, offset, offset, offset];
}
if (offset.length == 2) {
return offset.concat(offset);
}
else if (offset.length == 3) {
return offset.concat([offset[1]]);
}
return offset;
}
function offsetRect (rect, offset) {
if (!offset) {
return rect;
}
var offsetObject = {
top: isPercent(offset[0]) ? (parseFloat(offset[0]) * rect.height) : offset[0],
right: isPercent(offset[1]) ? (parseFloat(offset[1]) * rect.width) : offset[1],
bottom: isPercent(offset[2]) ? (parseFloat(offset[2]) * rect.height) : offset[2],
left: isPercent(offset[3]) ? (parseFloat(offset[3]) * rect.width) : offset[3]
};
Note: ClientRect object does not allow its properties to be written to therefore a new object has to be created.
return {
top: rect.top - offsetObject.top,
left: rect.left - offsetObject.left,
bottom: rect.bottom + offsetObject.bottom,
right: rect.right + offsetObject.right,
height: rect.height + offsetObject.top + offsetObject.bottom,
width: rect.width + offsetObject.left + offsetObject.right
};
}
function isPercent (n) {
return angular.isString(n) && n.indexOf('%') > 0;
}
A quick and dirty implementation of Rx to have a streamlined code in the directives.
didSubscribeFunc
: a function receiving a subscriber
as described belowUsage: var mySignal = new QuickSignal(function(subscriber) { … })
function QuickSignal (didSubscribeFunc) {
this.didSubscribeFunc = didSubscribeFunc;
}
Subscribe to a signal and consume the steam of data.
Returns a function that can be called to stop the signal stream of data and perform cleanup.
A subscriber
is a function that will be called when a new value arrives.
a subscriber.$dispose
property can be set to a function to be called uppon
disposal. When setting the $dispose
function, the previously set function
should be chained.
QuickSignal.prototype.subscribe = function (subscriber) {
this.didSubscribeFunc(subscriber);
var dispose = function () {
if (subscriber.$dispose) {
subscriber.$dispose();
subscriber.$dispose = null;
}
}
return dispose;
}
QuickSignal.prototype.map = function (f) {
var s = this;
return new QuickSignal(function (subscriber) {
subscriber.$dispose = s.subscribe(function (nextValue) {
subscriber(f(nextValue));
});
});
};
QuickSignal.prototype.filter = function (f) {
var s = this;
return new QuickSignal(function (subscriber) {
subscriber.$dispose = s.subscribe(function (nextValue) {
if (f(nextValue)) {
subscriber(nextValue);
}
});
});
};
QuickSignal.prototype.scan = function (initial, scanFunc) {
var s = this;
return new QuickSignal(function (subscriber) {
var last = initial;
subscriber.$dispose = s.subscribe(function (nextValue) {
last = scanFunc(last, nextValue);
subscriber(last);
});
});
}
QuickSignal.prototype.merge = function (signal) {
return signalMerge(this, signal);
};
QuickSignal.prototype.throttle = function (threshhold) {
var s = this, last, deferTimer;
return new QuickSignal(function (subscriber) {
var chainDisposable = s.subscribe(function () {
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
subscriber.apply(null, args);
}, threshhold);
} else {
last = now;
subscriber.apply(null, args);
}
});
subscriber.$dispose = function () {
clearTimeout(deferTimer);
if (chainDisposable) chainDisposable();
};
});
};
function signalMerge () {
var signals = arguments;
return new QuickSignal(function (subscriber) {
var disposables = [];
for (var i = signals.length - 1; i >= 0; i--) {
disposables.push(signals[i].subscribe(function () {
subscriber.apply(null, arguments);
}));
}
subscriber.$dispose = function () {
for (var i = disposables.length - 1; i >= 0; i--) {
if (disposables[i]) disposables[i]();
}
}
});
}
Returns a signal from DOM events of a target.
function signalFromEvent (target, event) {
return new QuickSignal(function (subscriber) {
var handler = function (e) {
subscriber(e);
};
var el = angular.element(target);
el.on(event, handler);
subscriber.$dispose = function () {
el.off(event, handler);
};
});
}
function signalSingle (value) {
return new QuickSignal(function (subscriber) {
setTimeout(function() { subscriber(value); });
});
}
Module loaders exports
if (typeof define === 'function' && define.amd) {
define(['angular'], moduleName);
} else if (typeof module !== 'undefined' && module && module.exports) {
module.exports = moduleName;
}
})();