KnockoutJS 或 Javascript 未正确跟踪显示的数组

KnockoutJS or Javascript not keeping proper track of displayed array

我正在写一个分页 table,底部有一个页面选择器,显示不同的页码

我正在使用淘汰赛。这些数字来自 ko.computed 数组 (self.pages),该数组根据每页的结果数/结果数计算有多少页。我 运行 遇到的问题是,如果数据数组很长并且每页的结果设置得有点低,我会得到这样的结果:

我想做的是将菜单项的数量限制为三个,因此如果选择第 4 页,则只有项目 3、4、5 可见。目前我正在实现第二个 ko.computed,它首先检索 self.pages 的值,然后获取当前页码 (self.pageNumber) 的值,并对数组进行切片,以便只有 3 个项目返回:

self.availablePages = ko.computed(function() {
  var pages = self.pages();
  var current = self.pageNumber();
  if (current === 0) {
    return pages.slice(current, current + 3);
  } else {
    return pages.slice(current - 1, current + 2);
  }
});

现在所有这一切似乎工作正常,但有一个错误我无法消除。使用敲除 css 数据绑定,我告诉它分配一个 class of 'selected' 给与 [=18 具有相同值的任何元素=](见下面的代码)。

如果选择的元素不需要self.availablePages改变(即在之前选择1的时候选择2),没有问题; 2 变为选中状态,1 变为未选中状态。

但是,如果选择确实需要 self.availablePages 更改(即 1、2、3 可见,选择 3 将可见变为 2、3、4),显示正确的数字,而不是 3被选中,4被选中。我假设这是因为 3 曾经位于 (last) 的数组索引现在被 4 占用了。

这是菜单:

<ul data-bind="foreach: availablePages">
  <li data-bind="if: $index() < 1">
    <a data-bind="click: $parent.toFirstPage">First</a>
  </li>
  <li>
    <a data-bind="text: displayValue, click: $parent.goToPage(iterator), css: { selected: $parent.pageNumber() === iterator }"></a>
  </li>
  <li data-bind="if: $parent.isLastIteration($index)">
    <a data-bind="click: $parent.toLastPage">Last</a>
  </li>
</ul>

被迭代的数组最初只是一个数字数组,但为了修复这个错误,我将其更改为以下对象的数组:

available.MenuModel = function(iterator) {
    var self = this;
    self.displayValue = iterator + 1;
    self.iterator = iterator;
    self.isSelected = ko.observable(false);
}

我尝试做的一件事是将 self.isSelected observable 添加到菜单中的所有项目,然后当 self.availablePages 被重新计算时,该函数检查 pageNumber 是什么,然后找到哪个数组中的项目匹配并设置 self.isSelected(true),然后尝试将 css 绑定到那个。

不幸的是,这没有用;它仍然有完全相同的错误。我一直在疯狂地调试脚本,但似乎没有问题;一切似乎都知道应该选择3,但实际上选择的是4。

我猜敲除绑定不够智能,跟不上这个。有什么我可以做的事情或某种模式可以帮助淘汰赛跟踪应该选择哪个元素吗?我什至尝试完全删除它,并且在脚本中手动有一个函数 remove/add 'selected' class 每当 self.pageNumber 被更改时and/or 每当 self.availablePages 改变但我仍然遇到同样的问题,所以也许这不是淘汰赛问题而是 javascript.

我已经尝试了所有我能想到的;订阅各种可观察对象、承诺,但就像我说的,一切都已经知道应该选择什么,所以额外的检查和回调不会改变任何东西,也不会消除错误。

我希望有人会知道错误的 cause/solution 或更聪明的方法来完成任务。这是 self.availablePages 关闭的 self.pages,以防有帮助:

    self.pages = ko.computed(function() {
        var start = self.totalPages();
        var pages = [];
        for (var i = 0; i < start + 1; ++i)
            pages.push(new available.MenuModel(i));
        return pages;
    });

这是整个 javascript 模型(使用 requireJs):

    define(['underscore', 'knockout'], function(_, ko) {

var available = available || {};

available.DynamicResponsiveModel = function(isDataObservable, isPaginated) {
        var self = this;
        self.workingArray = ko.observableArray([]);
        self.backgroundArray = ko.observableArray([]);
        self.pageNumber = ko.observable(0);
        self.count = function () {
            return 15;
        }
    self.resultsPerPage = ko.observable(self.count());
    self.selectResultsPerPage = [25, 50, 100, 200, 500];
    self.resultsPerPageOptions = ko.computed(function () {
        return self.selectResultsPerPage;
    });

    self.activeSortFunction = isDataObservable ? available.sortAlphaNumericObservable : available.sortAlphaNumeric;

    self.resetPageNumber = function() {
        self.pageNumber(0);
    }

    self.initialize = function(data) {
        var sortedList = data.sort(function(obj1, obj2) {
            return obj2.NumberOfServices - obj1.NumberOfServices;
        });
        self.workingArray(sortedList);
        self.backgroundArray(sortedList);
        self.pageNumber(0);
    }

    self.intializeWithoutSort = function(data) {
        self.workingArray(data);
        self.backgroundArray(data);
        self.pageNumber(0);
    }

    self.totalPages = ko.computed(function() {
        var num = Math.floor(self.workingArray().length / self.resultsPerPage());
        num += self.workingArray().length % self.resultsPerPage() > 0 ? 1 : 0;
        return num - 1;
    });

    self.paginated = ko.computed(function () {
        if (isPaginated) {
            var first = self.pageNumber() * self.resultsPerPage();
            return self.workingArray.slice(first, first + self.resultsPerPage());
        } else {
            return self.workingArray();
        }
    });

    self.pages = ko.computed(function() {
        var start = self.totalPages();
        var pages = [];
        for (var i = 0; i < start + 1; ++i)
            pages.push(new available.MenuModel(i));
        return pages;
    });

    self.availablePages = ko.computed(function() {
        var pages = self.pages();
        var current = self.pageNumber();
        if (current === 0) {
            return pages.slice(current, current + 3);
        } else {
            return pages.slice(current - 1, current + 2);
        }
    });

            self.pageNumDisplay = ko.computed(function() {
        return self.pageNumber() + 1;
    });

    self.hasPrevious = ko.computed(function() {
        return self.pageNumber() !== 0;
    });

    self.hasNext = ko.computed(function() {
        return self.pageNumber() !== self.totalPages();
    });

    self.next = function() {
        if (self.pageNumber() < self.totalPages()) {
            self.pageNumber(self.pageNumber() + 1);
        }
    }

    self.previous = function() {
        if (self.pageNumber() != 0) {
            self.pageNumber(self.pageNumber() - 1);
        }
    }

    self.toFirstPage = function() {
        self.pageNumber(0);
    }

    self.toLastPage = function() {
        self.pageNumber(self.totalPages());
    }

    self.setPage = function(data) {
        return new Promise(function(resolve, reject) {
            self.pageNumber(data);
        });
    }

    self.goToPage = function(data) {
        self.pageNumber(data);
    }

    self.isLastIteration = function (index) {
        var currentIndex = index();
        var count = self.pages().length;

        return currentIndex === count - 1;
    }

    self.resultsPerPage.subscribe(function() {
        self.pageNumber(0);
    });

    self.filterResults = function (filterFunction) {
        self.resetPageNumber();
        self.workingArray(filterFunction(self.backgroundArray()));
    }

    self.resetDisplayData = function() {
        self.workingArray(self.backgroundArray());
    }

    self.updateVisibleResults = function(data) {
        self.workingArray(data);
    }       
}

available.sortAlphaNumericObservable = function () {
    //...
}

available.sortAlphaNumeric = function () {
    //...
}

return available;
});

这是完整的 table:

<div data-bind="visible: showListOfEquipment, with: availableEquipmentModel">
<section class="panel panel-default table-dynamic">
    <table class="primary-table table-bordered">
        <thead>
            <tr>
                <th>
                    <div class="th">
                        Part Number    
                        <span class="fa fa-angle-up" data-bind="click: function () { sortByFirstColumn(false); }"></span>
                        <span class="fa fa-angle-down" data-bind="click: function () { sortByFirstColumn(true); }"></span>
                    </div>

                </th>
                <th>
                    <div class="th">
                        Serial Number
                        <span class="fa fa-angle-up" data-bind="click: function () { sortBySecondColumn(false); }"></span>
                        <span class="fa fa-angle-down" data-bind="click: function () { sortBySecondColumn(true); }"></span>
                    </div>
                </th>
                <th>
                    <div class="th">
                        Type
                        <span class="fa fa-angle-up" data-bind="click: function () { sortByThirdColumn(false); }"></span>
                        <span class="fa fa-angle-down" data-bind="click: function () { sortByThirdColumn(true); }"></span>
                    </div>
                </th>
                <th>
                    <div class="th">
                        Equipment Group
                        <span class="fa fa-angle-up" data-bind="click: function () { sortByFourthColumn(false); }"></span>
                        <span class="fa fa-angle-down" data-bind="click: function () { sortByFourthColumn(true); }"></span>
                    </div>
                </th>
                <th>
                    <div class="th">
                        Operational
                        <span class="fa fa-angle-up" data-bind="click: function () { sortByFifthColumn(false); }"></span>
                        <span class="fa fa-angle-down" data-bind="click: function () { sortByFifthColumn(true); }"></span>
                    </div>
                </th>
                <th>
                    <div class="th">
                        Valid
                        <span class="fa fa-angle-up" data-bind="click: function () { sortBySixthColumn(false); }"></span>
                        <span class="fa fa-angle-down" data-bind="click: function () { sortBySixthColumn(true); }"></span>
                    </div>
                </th>
            </tr>
        </thead>
        <tbody data-bind="foreach: paginated">
            <tr>
                <td data-bind="text: $data.PartNumber"></td>
                <td><a target="_blank" data-bind="text: $data.SerialNumber, click: function () { $root.setSerialNumberAndFindEquipment(SerialNumber) }" style="color:royalblue"></a></td>
                <td data-bind="text: $data.Type"></td>
                <td data-bind="text: $data.EquipmentGroup"></td>
                <td>
                    <span data-bind="css: $root.operationalCss($data), text: $root.getOpStatus($data)"></span>
                </td>
                <td data-bind="text: $data.Validity"></td>
            </tr>
        </tbody>
    </table>
    <footer class="table-footer">
        <div class="row">
            <div class="col-md-6 page-num-info">
                <span>Show <select style="min-width: 40px; max-width: 50px;" data-bind="options: selectResultsPerPage, value: resultsPerPage"></select> entries per page</span>
            </div>
            <div class="col-md-6 text-right pagination-container">
                <ul class="pagination-sm pagination" data-bind="foreach: pages">
                    <li data-bind="if: $index() < 1"><a data-bind="click: $parent.toFirstPage">First</a> </li>
                    <li class="paginationLi"><a data-bind="text: displayValue, click: $parent.goToPage(iterator), css: { selected: isSelected }"></a></li>
                    <li data-bind="if: $parent.isLastIteration($index)"> <a data-bind="click: $parent.toLastPage">Last</a> </li>
                </ul>
            </div>
        </div>
    </footer>
</section>


我继续构建了一个分页器。我没有像你那样使用数组,而是只使用了可用页面的数量,pageCount.

可能唯一值得更详细研究的是计算要显示的页面:

this.visiblePages = ko.computed(function() {
  var previousHalf = Math.floor( (this.visiblePageCount() - 1) / 2 ),
      nextHalf     = Math.ceil( (this.visiblePageCount() - 1) / 2 ),
      visiblePages = [],
      firstPage,
      lastPage;

  // too close to the beginning
  if ( this.currentPage() - previousHalf < 1 ) {
    firstPage = 1;
    lastPage  = this.visiblePageCount();

    if ( lastPage > this.pageCount() ) {
      lastPage = this.pageCount();
    }

  // too close to the end
  } else if ( this.currentPage() + nextHalf > this.pageCount() ) {
    lastPage  = this.pageCount();
    firstPage = this.pageCount() - this.visiblePageCount() + 1;

    if (firstPage < 1) {
      firstPage = 1;
    }

  // just right
  } else {
    firstPage = this.currentPage() - previousHalf;
    lastPage  = this.currentPage() + nextHalf;
  }

  for (var i = firstPage; i <= lastPage; i++) {
    visiblePages.push(i);
  }

  return visiblePages;

}, this);

让我们一块一块地过一遍。我们希望当前页面位于所有显示的分页按钮的中间,一些在其左侧,一些在其右侧。但是有多少?

如果我们使用奇数,例如三,这很简单:数字减去 1(选中的那个)除以二。 (3 - 1) / 2 = 1,或者每边一个。

要显示偶数个分页按钮,这是行不通的,因此我们分别计算每一侧并将结果四舍五入:

var previousHalf = Math.floor( (this.visiblePageCount() - 1) / 2 ),
    nextHalf     = Math.ceil( (this.visiblePageCount() - 1) / 2 ),

三种可能的结果:

  1. 我们的选择合适
  2. 我们离开始太近了
  3. 我们离结束太近了

如果我们离开始太近了:

if ( this.currentPage() - previousHalf < 1 ) {
  firstPage = 1;
  lastPage  = this.visiblePageCount();

  if ( lastPage > this.pageCount() ) {
    lastPage = this.pageCount();
  }

}

我们从 1 开始,并尝试显示第 1 页到 visiblePageCount。如果这也不起作用,因为我们没有足够的页面,我们只显示我们所有的。

如果我们离结束太近了:

  } else if ( this.currentPage() + nextHalf > this.pageCount() ) {
    lastPage  = this.pageCount();
    firstPage = this.pageCount() - this.visiblePageCount() + 1;

    if (firstPage < 1) {
      firstPage = 1;
    }
  }

我们以最后一页结束,并尝试在左侧显示尽可能多的页面。如果这不起作用,因为我们没有足够的页面,我们只显示我们所有的。

完整示例如下:

var ViewModel;

ViewModel = function ViewModel() {
  var that = this;
  
  this.pageCount        = ko.observable(20);
  this.currentPage      = ko.observable(1);
  this.visiblePageCount = ko.observable(3);
  
  this.gotoPage = function gotoPage(page) {
    that.currentPage(page);
  };
  
  this.visiblePages = ko.computed(function() {
    var previousHalf = Math.floor( (this.visiblePageCount() - 1) / 2 ),
        nextHalf     = Math.ceil( (this.visiblePageCount() - 1) / 2 ),
        visiblePages = [],
        firstPage,
        lastPage;
    
    if ( this.currentPage() - previousHalf < 1 ) {
      firstPage = 1;
      lastPage  = this.visiblePageCount();
      
      if ( lastPage > this.pageCount() ) {
        lastPage = this.pageCount();
      }
      
    } else if ( this.currentPage() + nextHalf > this.pageCount() ) {
      lastPage  = this.pageCount();
      firstPage = this.pageCount() - this.visiblePageCount() + 1;
      
      if (firstPage < 1) {
        firstPage = 1;
      }
      
    } else {
      firstPage = this.currentPage() - previousHalf;
      lastPage  = this.currentPage() + nextHalf;
    }
    
    for (var i = firstPage; i <= lastPage; i++) {
      visiblePages.push(i);
    }
    
    return visiblePages;
    
  }, this);
  
};

ko.applyBindings( new ViewModel() );
ul {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  
  margin: 0;
  padding: 0;
  list-style-type: none;
}

ul li {
 -webkit-box-flex: 0;
 -webkit-flex: 0 0 auto;
     -ms-flex: 0 0 auto;
         flex: 0 0 auto;
}

button {
  margin-right: 0.5rem;
  padding: 0.5rem;
  background-color: lightgrey;
  border: none;
}

button.selected {
  background-color: lightblue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul>
  <li><button data-bind="click: gotoPage.bind($data, 1)">First</button></li>
  <!-- ko foreach: visiblePages -->
    <li>
      <button data-bind="text: $data,
                         click: $parent.gotoPage,
                         css: { selected: $parent.currentPage() === $data }"></button>
    </li>
  <!-- /ko -->
  <li><button data-bind="click: gotoPage.bind($data, pageCount())">Last</button></li>
</ul>