先上图看下排序后的效果:
问题背景
最近做公司业务使用到了 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;
}