123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- // # Angular-Inview
- // - Author: [Nicola Peduzzi](https://github.com/thenikso)
- // - Repository: https://github.com/thenikso/angular-inview
- // - Install with: `npm install angular-inview`
- // - Version: **2.2.0**
- (function() {
- 'use strict';
- // An [angular.js](https://angularjs.org) 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 angularInviewModule = angular.module('angular-inview', [])
- // ## in-view directive
- //
- // ### Usage
- // ```html
- // <any in-view="{expression}" [in-view-options="{object}"]></any>
- // ```
- .directive('inView', ['$parse', inViewDirective])
- // ## in-view-container directive
- .directive('inViewContainer', inViewContainerDirective);
- // ## Implementation
- 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'));
- // Merged with the page visibility events
- if (options.considerPageVisibility) {
- viewportEventSignal = viewportEventSignal.merge(signalFromEvent(document, 'visibilitychange'));
- }
- // Merged with the page focus/blur events
- if (options.considerPageFocus) {
- viewportEventSignal = viewportEventSignal.merge(signalFromEvent(window, 'focus blur'));
- }
- // 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 isVisible = !!(element[0].offsetWidth || element[0].offsetHeight || element[0].getClientRects().length);
- var documentVisible = !options.considerPageVisibility || document.visibilityState === 'visible' || document.hidden === false;
- var documentFocussed = !options.considerPageFocus || document.hasFocus();
- var info = {
- inView: documentVisible && documentFocussed && isVisible && 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();
- };
- }]
- }
- }
- // ## Utilities
- 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;
- }
- // ## QuickSignal FRP
- // A quick and dirty implementation of Rx to have a streamlined code in the
- // directives.
- // ### QuickSignal
- //
- // - `didSubscribeFunc`: a function receiving a `subscriber` as described below
- //
- // Usage:
- // 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'], angularInviewModule);
- } else if (typeof module !== 'undefined' && module && module.exports) {
- module.exports = angularInviewModule;
- }
- })();
|