瀏覽代碼

Use infinite-scrolling on lists

This allows to reduce the loading time of a big board.

Note that there is an infinite scroll implementation in the mixins,
but this doesn't fit well as the cards in the list can have arbitrary
height.

The idea to rely on the visibility of a spinner is based on
http://www.meteorpedia.com/read/Infinite_Scrolling
Benjamin Tissoires 6 年之前
父節點
當前提交
66bc1f28dd
共有 3 個文件被更改,包括 95 次插入1 次删除
  1. 3 0
      client/components/lists/list.styl
  2. 11 1
      client/components/lists/listBody.jade
  3. 81 0
      client/components/lists/listBody.js

+ 3 - 0
client/components/lists/list.styl

@@ -211,6 +211,9 @@
   max-height: 250px
   max-height: 250px
   overflow: hidden
   overflow: hidden
 
 
+.sk-spinner-list
+  margin-top: unset !important
+
 list-header-color(background, color...)
 list-header-color(background, color...)
   border-bottom: 6px solid background
   border-bottom: 6px solid background
 
 

+ 11 - 1
client/components/lists/listBody.jade

@@ -4,7 +4,7 @@ template(name="listBody")
       if cards.count
       if cards.count
         +inlinedForm(autoclose=false position="top")
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
           +addCardForm(listId=_id position="top")
-      each (cards (idOrNull ../../_id))
+      each (cardsWithLimit (idOrNull ../../_id))
         a.minicard-wrapper.js-minicard(href=absoluteUrl
         a.minicard-wrapper.js-minicard(href=absoluteUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
@@ -12,6 +12,16 @@ template(name="listBody")
             .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
             .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
               class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
               class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           +minicard(this)
           +minicard(this)
+      if (showSpinner (idOrNull ../../_id))
+        .sk-spinner.sk-spinner-wave.sk-spinner-list(
+          class=currentBoard.colorClass
+          id="showMoreResults")
+          .sk-rect1
+          .sk-rect2
+          .sk-rect3
+          .sk-rect4
+          .sk-rect5
+
       if canSeeAddCard
       if canSeeAddCard
         +inlinedForm(autoclose=false position="bottom")
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")
           +addCardForm(listId=_id position="bottom")

+ 81 - 0
client/components/lists/listBody.js

@@ -1,6 +1,34 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
+const InfiniteScrollIter = 10;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
+  onCreated() {
+    // for infinite scrolling
+    this.cardlimit = new ReactiveVar(InfiniteScrollIter);
+  },
+
+  onRendered() {
+    const domElement = this.find('.js-perfect-scrollbar');
+
+    this.$(domElement).on('scroll', () => this.updateList(domElement));
+    $(window).on(`resize.${this.data().listId}`, () => this.updateList(domElement));
+
+    // we add a Mutation Observer to allow propagations of cardlimit
+    // when the spinner stays in the current view (infinite scrolling)
+    this.mutationObserver = new MutationObserver(() => this.updateList(domElement));
+
+    this.mutationObserver.observe(domElement, {
+      childList: true,
+    });
+
+    this.updateList(domElement);
+  },
+
+  onDestroyed() {
+    $(window).off(`resize.${this.data().listId}`);
+    this.mutationObserver.disconnect();
+  },
+
   mixins() {
   mixins() {
     return [Mixins.PerfectScrollbar];
     return [Mixins.PerfectScrollbar];
   },
   },
@@ -60,6 +88,13 @@ BlazeComponent.extendComponent({
         type: 'cardType-card',
         type: 'cardType-card',
       });
       });
 
 
+      // if the displayed card count is less than the total cards in the list,
+      // we need to increment the displayed card count to prevent the spinner
+      // to appear
+      const cardCount = this.data().cards(this.idOrNull(swimlaneId)).count();
+      if (this.cardlimit.get() < cardCount) {
+        this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
+      }
 
 
       // In case the filter is active we need to add the newly inserted card in
       // In case the filter is active we need to add the newly inserted card in
       // the list of exceptions -- cards that are not filtered. Otherwise the
       // the list of exceptions -- cards that are not filtered. Otherwise the
@@ -119,6 +154,52 @@ BlazeComponent.extendComponent({
     return undefined;
     return undefined;
   },
   },
 
 
+  cardsWithLimit(swimlaneId) {
+    const limit = this.cardlimit.get();
+    const selector = {
+      listId: this.currentData()._id,
+      archived: false,
+    };
+    if (swimlaneId)
+      selector.swimlaneId = swimlaneId;
+    return Cards.find(Filter.mongoSelector(selector), {
+      sort: ['sort'],
+      limit,
+    });
+  },
+
+  spinnerInView(container) {
+    const parentViewHeight = container.clientHeight;
+    const bottomViewPosition = container.scrollTop + parentViewHeight;
+
+    const spinner = this.find('.sk-spinner-list');
+
+    const threshold = spinner.offsetTop;
+
+    return bottomViewPosition > threshold;
+  },
+
+  showSpinner(swimlaneId) {
+    const list = Template.currentData();
+    return list.cards(swimlaneId).count() > this.cardlimit.get();
+  },
+
+  updateList(container) {
+    // first, if the spinner is not rendered, we have reached the end of
+    // the list of cards, so skip and disable firing the events
+    const target = this.find('.sk-spinner-list');
+    if (!target) {
+      this.$(container).off('scroll');
+      $(window).off(`resize.${this.data().listId}`);
+      return;
+    }
+
+    if (this.spinnerInView(container)) {
+      this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
+      Ps.update(container);
+    }
+  },
+
   canSeeAddCard() {
   canSeeAddCard() {
     return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
     return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
   },
   },