瀏覽代碼

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
   overflow: hidden
 
+.sk-spinner-list
+  margin-top: unset !important
+
 list-header-color(background, color...)
   border-bottom: 6px solid background
 

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

@@ -4,7 +4,7 @@ template(name="listBody")
       if cards.count
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
-      each (cards (idOrNull ../../_id))
+      each (cardsWithLimit (idOrNull ../../_id))
         a.minicard-wrapper.js-minicard(href=absoluteUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
@@ -12,6 +12,16 @@ template(name="listBody")
             .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
               class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           +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
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")

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

@@ -1,6 +1,34 @@
 const subManager = new SubsManager();
+const InfiniteScrollIter = 10;
 
 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() {
     return [Mixins.PerfectScrollbar];
   },
@@ -60,6 +88,13 @@ BlazeComponent.extendComponent({
         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
       // the list of exceptions -- cards that are not filtered. Otherwise the
@@ -119,6 +154,52 @@ BlazeComponent.extendComponent({
     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() {
     return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
   },