fullcalendar 严格按照给定顺序排序的一种实现

1,650 阅读3分钟

先上图看下排序后的效果:

问题背景

最近做公司业务使用到了 fullcalendar 插件。发现排序功能不能满足业务需求。通过 fullcalendar 文档中的 “eventOrder” 得出的排序在某些场景不符合预期。比如全天事件中有混合的跨天事件。在排序过程中会有“补位”的现象。导致没有按照给定的顺序排序。

fullcalendar 版本: v4.4.0

源码解读

// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
        // NOTE: modifies segs
        DayGridEventRenderer.prototype.buildSegLevels = function (segs) {
          var isRtl = this.context.isRtl;
          var colCnt = this.dayGrid.colCnt;
          var levels = [];
          var i;
          var seg;
          var j;
          // Give preference to elements with certain criteria, so they have
          // a chance to be closer to the top.
1         segs = this.sortEventSegs(segs);
          for (i = 0; i < segs.length; i++) {
            seg = segs[i];

            // loop through levels, starting with the topmost, until the segment doesn`t collide with other segments
            for (j = 0; j < levels.length; j++) {
2             if (!isDaySegCollision(seg, levels[j])) {
                break;
              }
            }
            // `j` now holds the desired subrow index
            seg.level = j;
            seg.leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; // for sorting only
            seg.rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; // for sorting only
            (levels[j] || (levels[j] = [])).push(seg);
          }
          // order segments left-to-right. very important if calendar is RTL
          for (j = 0; j < levels.length; j++) {
3           levels[j].sort(compareDaySegCols);
          }
          return levels;
        };

这是fullcalendar月视图排序的核心源码。他是默认采用“逐行分层补位”算法。当然,这个"逐行分层补位"是我根据个人理解给起的别称。所谓的“逐行”是fullcalendar在渲染事件(events)前分组排序处理的分组单元。是按照每周一行进行渲染的。而不是我们常规理解的一个td单元格/天进行渲染的。

levels 是一行,也可以称一周,中所有事件按照层级展示的二维数组集合。

在源码标识1行处,segs 得到的数据,实际上已经是通过插件暴露给我们的 “eventOrder” 配置处理后的数组了。

但实际上 标识2行处 “isDaySegCollision” 函数会判断遍历的当前事件是否和已排序的当前行冲突。

// Computes whether two segments columns collide. They are assumed to be in the same row.
    function isDaySegCollision(seg, otherSegs) {
        var i;
        var otherSeg;
        for (i = 0; i < otherSegs.length; i++) {
            otherSeg = otherSegs[i];
            if (otherSeg.firstCol <= seg.lastCol &&
                otherSeg.lastCol >= seg.firstCol) {
                return true;
            }
        }
        return false;
    }

如不冲突,则如图,事件 “排序3”会补位到第一层(levels[0])中,无视排序。这也是考虑为了充分利用展示空间。如果这种“补位”实在不符合业务场景。那么就要在标识2处的if条件中添加判断,从而防止“补位”动作。

一种实现方法

首先要在遍历事件segs时做如下记录:

for (i = 0; i < segs.length; i++) {
    ...something
    for (z = seg.firstCol; z <= seg.lastCol; z++) {
      cells[seg.row + '-' + z] = seg.level;
    }
}

cells 是以单元格td为基本单位,记录当前单元格的事件层级数值。

标识2处 if判断:

if (!isDaySegCollision(seg, levels[j]) && j >= (cells[seg.row + '-' + seg.firstCol] || 0)) {
    break;
  }

原理是:遍历的当前事件与排好的levels[j]是否冲突。如果不冲突,并且当前层级j大于等于记录的当前单元格最大层级数值,那么执行break;不再增加层级j,将当前的事件插入到levels[j]队列中。

完整代码如下:

DayGridEventRenderer.prototype.buildSegLevels = function (segs) {
      var isRtl = this.context.isRtl;
      var colCnt = this.dayGrid.colCnt;
      var levels = [];
      var i;
      var seg;
      var j;
      var z;  // 新增
      var cells = {}; // 新增
      // Give preference to elements with certain criteria, so they have
      // a chance to be closer to the top.
      segs = this.sortEventSegs(segs);
      for (i = 0; i < segs.length; i++) {
          seg = segs[i];

          // loop through levels, starting with the topmost, until the segment doesn`t collide with other segments
          for (j = 0; j < levels.length; j++) {
              // 新增 j > (cells[`${seg.row}-${seg.firstCol}`] || 0)
              if (!isDaySegCollision(seg, levels[j]) && j >= (cells[`${seg.row}-${seg.firstCol}`] || 0)) {
                  // console.log('cells2', cells, j, seg.eventRange.def.extendedProps.orderNum, seg);
                  break;
              }
          }
          // `j` now holds the desired subrow index
          seg.level = j;
          seg.leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; // for sorting only
          seg.rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; // for sorting only
          (levels[j] || (levels[j] = [])).push(seg);

          // 新增 记录
          for (z = seg.firstCol; z <= seg.lastCol; z++) {
              cells[`${seg.row}-${z}`] = seg.level;
          }
      }
      // order segments left-to-right. very important if calendar is RTL
      for (j = 0; j < levels.length; j++) {
          levels[j].sort(compareDaySegCols);
      }
      return levels;
   }