angular-ui / ui-scroll

Unlimited bidirectional scrolling over a limited element buffer for AngularJS applications
http://angular-ui.github.io/ui-scroll/demo/
MIT License
326 stars 107 forks source link

Different uiScrollViewport schemes when using as a component #239

Open ornic opened 4 years ago

ornic commented 4 years ago

I am trying to use great ui-scroll module inside one of the components. This is a (surprise!) list, but i need to show it both inside window (no viewport) or inside popup (with viewport).

The only way I know to tell ui-scroll about viewport is ui-scroll-viewport attribute. And the only way I can tell Angular about optional attribute is ng-attr- prefix. I want it to be optional attribute, since I hate to copy-paste the same code in ng-switches.

And this is a place when things get interesting. According to https://github.com/angular/angular.js/issues/16441 ui-scroll gets information about ui-scroll-viewport attribute before evaluation of ng-attr-ui-scroll-viewport value.

May be someone will point me to more "right" solution. But for now I had to patch ui-scroll.js file.

1) I send the max-height style inside some param to the component

    <ul class="list-group" style="overflow-x: hidden" ng-attr-ui-scroll-viewport="$ctrl.checkRestrictHeight()" ng-style="{'min-height': '1px', 'max-height': $ctrl.checkRestrictHeight()}">

        <a class="list-group-item" ui-scroll="survey in $ctrl.itemsSource" start-index="$ctrl.startPosition" buffer-size="30" adapter="$ctrl.scroll"
...

2) I check the result of $eval for ng-attr-ui-scroll-viewport value

function Viewport(elementRoutines, buffer, element, viewportController, $rootScope, padding) {
  var topPadding = null;
  var bottomPadding = null;

    //ADD

    let ngAttrEval = true;
    if (viewportController && viewportController.viewport && viewportController.viewport[0].attributes && viewportController.viewport[0].attributes.hasOwnProperty('ng-attr-ui-scroll-viewport') && !viewportController.viewport[0].attributes.hasOwnProperty('ui-scroll-viewport')) {
        ngAttrEval = false;
        if (viewportController.scope) {
            ngAttrEval = viewportController.scope.$eval(viewportController.viewport[0].attributes['ng-attr-ui-scroll-viewport'].value);
        }
    }

    //CHANGE

    var viewport = viewportController && viewportController.viewport && ngAttrEval ? viewportController.viewport : angular.element(window);
    var container = viewportController && viewportController.container && ngAttrEval ? viewportController.container : undefined;
    var scope = viewportController && viewportController.scope && ngAttrEval ? viewportController.scope : $rootScope;

...
dhilt commented 4 years ago

@ornic If you are going to augment Viewport module, an easier approach might be as follows.

Template:

<ul class="list-group" ui-scroll-viewport="$ctrl.checkRestrictHeight()">

Directive:

.directive('uiScrollViewport', function () {
  return {
    restrict: 'A',
    scope: { window: '=uiScrollViewport' },   // new line, assign directive attr value to scope.window
    controller: [
    // ...

Viewport:

const ctrl = viewportController;
const isViewport = ctrl && ctrl.scope && ctrl.scope.window !== false;
const scope = isViewport ? ctrl.scope : $rootScope;
const viewport = isViewport && ctrl.viewport ? ctrl.viewport : angular.element(window);
const container = isViewport && ctrl.container ? ctrl.container : undefined;

Note, the explicit check is used: !== false -- it is necessary for the case when a user doesn't pass any value to the "ui-scroll-viewport" attribute but expects the Viewport to be present.

ornic commented 4 years ago

This way code looks more readable than in my crude variant. But I found that after defining scope in .directive it is unable to make an adaptor in viewportController.scope, but constantly setting viewport.scope to $rootScope fixed the problem (for me, with using ui-scroll via component). But I assume that in future (more usecases inside one app) this approach may backfire.

UPD: I looked further and it seems that this approach should be Ok, since the only result AFAIK is always using scope of uiScroll directive.

dhilt commented 4 years ago

I didn't meet any issues with the Adapter when using the suggested approach.

ornic commented 4 years ago

I also found the very handy method of using full-window scroll: with some block of text before the list. But there is a problem on the way.

This is the easy correction of your example. This div goes before the list

<div style="
    height: 300px;
    text-align: center;
    border: 1px solid lightgreen;
    vertical-align: middle;
    padding: 100px;
">Hello! I'm 300px of height.</div>

and we limit the list with index -100.

And then we lose ability to correctly identify first and last element: image

I'm trying to find the way to overcome this. :)

....

That was... interesting :))) As a result I adjusted *Pos functions with an idea to use container top offset and scrollHeight to compensate:

bottomDataPos: function bottomDataPos() {
    var scrollHeight = viewport[0].scrollHeight;
    scrollHeight = scrollHeight != null ? scrollHeight : viewport[0].document.documentElement.scrollHeight;
    let btmPadding = bottomPadding.height();
    if (container && container[0] != viewport[0]) { // the only possible case: viewport = window && container != window
        if (btmPadding > 0) { // consider space under the container
            return container[0].scrollHeight - btmPadding;
        }
        scrollHeight -= container.offset().top;
    }
    return scrollHeight - btmPadding;
},
topDataPos: function topDataPos() {
    let topDataPos = topPadding.height();
    return topDataPos;
},
bottomVisiblePos: function bottomVisiblePos() {
    let viewPortHeight = viewport.outerHeight();
    if (container && container[0] != viewport[0]) {
        let offset = container.offset().top - viewport.scrollTop();
        if (offset > 0) viewPortHeight -= offset;
    }
    return viewport.topVisiblePos() + viewPortHeight;
},
topVisiblePos: function topVisiblePos() {
    let topVisiblePos = viewport.scrollTop();
    if (container && container[0] != viewport[0]) {
        topVisiblePos -= container.offset().top;
        if (topVisiblePos < 0) topVisiblePos = 0; // no negative positions :)
    }
    return topVisiblePos;
}