docs: function inViewDirective ($parse) {
    return {
      // Evaluate the expression passed 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 isVisible = !!(element[0].offsetWidth || element[0].offsetHeight || element[0].getClientRects().length);
          var info = {
            inView: 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);
      }
    }
  }