兼容两种Table Layout算法的表格列宽分配算法

兼容两种Table Layout算法的表格列宽分配算法

1. table-layout 布局算法介绍

(1) auto

表格及单元格的宽度取决于其包含的内容。大多数浏览器采用自动表格布局算法对表格布局。

(2) fixed

  • 表格和列的宽度通过表格的宽度来设置,某一列的宽度仅由该列首行的单元格决定
  • 整个表格可以在其首行被下载后就被解析和渲染
  • 对于“automatic”自动布局方式来说可以加速渲染,但是其后的单元格内容并不会自适应当前列宽
  • 任何一个包含溢出内容的单元格可以使用 overflow 属性控制是否允许内容溢出

2. auto模式下的表格列宽调整逻辑

自动宽度布局,速度上虽然不如前者,但它是表格的默认宽度布局方案。慢的原因在于表格会对每一个单元格宽度进行计算,每个单元格的宽度由内容决定。

宽度计算规则:

  • 获取表格中每个column的宽度,column的宽度由每个单元格宽度决定,每个单元格宽度又由单元格内的内容决定,计算每行某列的单元格,并获取某列单元格的最小值和最大值。
  • 设表格的宽度为width,所有列的宽度和为AllcolWidth,遍历每一行。

若width为auto,则表格宽度就是由所有列总和值决定,每列的宽度由自身的内容决定。width = AllcolWidth

  1. 若每列的宽度为auto,每列的宽度由内容决定,计算出每行每列宽度的最大值和最小值,某列的宽度由某行的最大值决定。
  2. 若部分列宽度为固定值,AllcolWidth = 未固定列宽为某行的最大值 + 所设置的宽度
  3. 若每列都有固定的宽度值,表格的宽度即为 AllcolWidth = 各固定值之和

若width为100%(宽度为父元素的值)或为固定值

  1. 若每列的宽度为auto,每列的宽度由内容决定,计算出每行每列宽度的最大值和最小值,根据这些最大值和最小值计算列宽。
  2. 若部分列宽度为固定值,AllcolWidth = 未固定列宽自适应计算的宽度(在最大值和最小值之间) + 所设置的宽度
  3. 若每列都有固定的宽度值,需要计算出column宽度、border与单元格间距的总和为AllColWidth
    1. width > AllColWidth,多余的宽度会均分给所有列。
    2. width < AllColWidth,每个column,单元格进行width/AllColWidth比例的缩小。

3. fixed模式下的表格列宽调整逻辑

固宽布局必须要设置:width: 100%width: 固定值

根据表格的宽度 width 和第一行所有列宽的总和 colWidth ,也就是说表格在第一行被下载后就被解析和渲染,后面行的列宽以第一行为准。

宽度计算规则:第一行的所有列宽由<colgroup>中的<col>或单元格的 width 属性决定。

  1. 其他列的宽度均为auto,将根据列数平均分配,各列宽度相同
  2. 表格中某几列宽度设置了固定宽度,这几列宽度总和为partColWidth,其它列将均分剩余宽度,公式为(table-width – partColWidth)/ 剩下的列数
  3. 若第一行所有列的宽度都为固定,所有列宽度总和设为ColWidth,表格宽度为width。
    若ColWidth >= width,则表格宽度为ColWidth,所有的列的宽度为自己设置的宽度;
    若ColWidth < width,则表格宽度为width,所有列的宽度将重新按比例分配宽度,使更新后的colWidth与width相等,即 (自己设置的宽度 / ColWidth) * width
  4. 若有列合并,根据以上规则得出此列的宽度,合并列里面的列平分合并后的宽度。

对于td单元格,当table设置 white-space: nowarp; 是,fixed模式会有内容溢出的问题,直接可以使用overflow: hidden 隐藏溢出内容。或者使用 word-break: break-all; word-wrap: break-word; 强制换行。如果想查看单元格里面的内容,overflow: scroll,可在单元格里面滚动查看内容。

4. 兼容table-layout的表格列宽分配算法

const recalculateColWidth = (
  columns: BaseTableCol[],
  thWidthList: { [colKey: string]: number },
  tableLayout: string,
  tableElmWidth: number,
): void => {
  let actualWidth = 0;
  const missingWidthCols: BaseTableCol[] = [];
  const thMap: { [colKey: string]: number } = {};

  // 计算现有列的列宽总和
  columns.forEach((col) => {
    if (!thWidthList[col.colKey]) {
      thMap[col.colKey] = isNumber(col.width) ? col.width : parseFloat(col.width);
    } else {
      thMap[col.colKey] = thWidthList[col.colKey];
    }
    const originWidth = thMap[col.colKey];
    if (originWidth) {
      actualWidth += originWidth;
    } else {
      missingWidthCols.push(col);
    }
  });

  let tableWidth = tableElmWidth;
  let needUpdate = false;
  // 表宽没有初始化时,默认给没有指定列宽的列指定宽度为100px
  if (tableWidth > 0) {
    // 存在没有指定列宽的列
    if (missingWidthCols.length) {
      // 当前列宽总宽度小于表宽,将剩余宽度平均分配给未指定宽度的列
      if (actualWidth < tableWidth) {
        const widthDiff = tableWidth - actualWidth;
        const avgWidth = widthDiff / missingWidthCols.length;
        missingWidthCols.forEach((col) => {
          thMap[col.colKey] = avgWidth;
        });
      } else if (tableLayout === 'fixed') {
        // 当前列表总宽度大于等于表宽,且当前排版模式为fixed,默认填充100px
        missingWidthCols.forEach((col) => {
          const originWidth = thMap[col.colKey] || 100;
          thMap[col.colKey] = isNumber(originWidth) ? originWidth : parseFloat(originWidth);
        });
      } else {
        // 当前列表总宽度大于等于表宽,且当前排版模式为auto,默认填充100px,然后按比例重新分配各列宽度
        const extraWidth = missingWidthCols.length * 100;
        const totalWidth = extraWidth + actualWidth;
        columns.forEach((col) => {
          if (!thMap[col.colKey]) {
            thMap[col.colKey] = (100 / totalWidth) * tableWidth;
          } else {
            thMap[col.colKey] = (thMap[col.colKey] / totalWidth) * tableWidth;
          }
        });
      }
      needUpdate = true;
    } else {
      // 所有列都已经指定宽度
      if (notCalculateWidthCols.value.length) {
        // 存在不允许重新计算宽度的列(一般是resize后的两列),这些列不参与后续计算
        let sum = 0;
        notCalculateWidthCols.value.forEach((colKey) => {
          sum += thMap[colKey];
        });
        actualWidth -= sum;
        tableWidth -= sum;
      }
      // 重新计算其他列的宽度,按表格剩余宽度进行按比例分配
      if (actualWidth !== tableWidth || notCalculateWidthCols.value.length) {
        columns.forEach((col) => {
          if (notCalculateWidthCols.value.includes(col.colKey)) return;
          thMap[col.colKey] = (thMap[col.colKey] / actualWidth) * tableWidth;
        });
        needUpdate = true;
      }
    }
  } else {
    // 表格宽度未初始化,默认填充100px
    missingWidthCols.forEach((col) => {
      const originWidth = thMap[col.colKey] || 100;
      thMap[col.colKey] = isNumber(originWidth) ? originWidth : parseFloat(originWidth);
    });

    needUpdate = true;
  }

  // 更新列存储值
  updateThWidthList(thMap);

  if (notCalculateWidthCols.value.length) {
    notCalculateWidthCols.value = [];
  }
};

当前算法已在 Tencent/tdesign-vue 仓库中实现,具体效果可以参考 TDesign 官方示例。

5. 参考文章

[1] https://css-tricks.com/fixing-tables-long-strings/

[2] https://x-front-team.github.io/2017/04/25/table-layout%E7%90%86%E8%A7%A3%E5%88%B0%E6%94%BE%E5%BC%83/