@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
347 lines (333 loc) • 13.4 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import { maphElem } from '@atlaskit/editor-common/utils';
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
import { TableMap } from '@atlaskit/editor-tables/table-map';
import { findTable, getSelectionRect } from '@atlaskit/editor-tables/utils';
import { tableDeleteButtonSize } from '../../ui/consts';
export var getColumnsWidths = function getColumnsWidths(view) {
var selection = view.state.selection;
var widths = [];
var table = findTable(selection);
if (table) {
var map = TableMap.get(table.node);
var domAtPos = view.domAtPos.bind(view);
// When there is no cell we need to fill it with undefined
widths = Array.from({
length: map.width
});
for (var i = 0; i < map.width; i++) {
if (!map.isCellMergedTopLeft(0, i)) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var node = table.node.nodeAt(map.map[i]);
var pos = map.map[i] + table.start;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
var cellRef = findDomRefAtPos(pos, domAtPos);
var rect = cellRef.getBoundingClientRect();
widths[i] = (rect ? rect.width : cellRef.offsetWidth) + 1;
i += node.attrs.colspan - 1;
}
}
}
return widths;
};
/**
* Splits a merged cell's rendered width by `colwidth` ratios, falling back to an even split when
* usable ratios are unavailable.
*/
export var getProportionalColumnWidths = function getProportionalColumnWidths(totalWidth, columnCount, ratios) {
var evenSplit = function evenSplit() {
return new Array(columnCount).fill(totalWidth / columnCount);
};
if (!ratios || ratios.length !== columnCount) {
return evenSplit();
}
var total = ratios.reduce(function (sum, ratio) {
return sum + (ratio > 0 ? ratio : 0);
}, 0);
if (total <= 0) {
return evenSplit();
}
return ratios.map(function (ratio) {
return totalWidth * ((ratio > 0 ? ratio : 0) / total);
});
};
/**
* Like `getColumnsWidths`, but fills every visual column under a first-row `colspan` so column
* controls can render one grid track per column.
*/
export var getColumnsWidthsWithMergedCells = function getColumnsWidthsWithMergedCells(view) {
var selection = view.state.selection;
var table = findTable(selection);
if (!table) {
return [];
}
var map = TableMap.get(table.node);
var domAtPos = view.domAtPos.bind(view);
var widths = Array.from({
length: map.width
});
for (var i = 0; i < map.width; i++) {
var _node$attrs$colspan;
if (map.isCellMergedTopLeft(0, i)) {
continue;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var node = table.node.nodeAt(map.map[i]);
var pos = map.map[i] + table.start;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
var cellRef = findDomRefAtPos(pos, domAtPos);
var rect = cellRef.getBoundingClientRect();
var measuredWidth = (rect ? rect.width : cellRef.offsetWidth) + 1;
var colspan = (_node$attrs$colspan = node.attrs.colspan) !== null && _node$attrs$colspan !== void 0 ? _node$attrs$colspan : 1;
if (colspan <= 1) {
widths[i] = measuredWidth;
continue;
}
var colwidth = Array.isArray(node.attrs.colwidth) ? node.attrs.colwidth : undefined;
var perColumnWidths = getProportionalColumnWidths(measuredWidth, colspan, colwidth);
for (var span = 0; span < colspan && i + span < map.width; span++) {
widths[i + span] = perColumnWidths[span];
}
i += colspan - 1;
}
return widths;
};
export var getColumnDeleteButtonParams = function getColumnDeleteButtonParams(columnsWidths, selection) {
var rect = getSelectionRect(selection);
if (!rect) {
return null;
}
var width = 0;
var offset = 0;
// find the columns before the selection
for (var i = 0; i < rect.left; i++) {
var colWidth = columnsWidths[i];
if (colWidth) {
offset += colWidth - 1;
}
}
// these are the selected columns widths
var indexes = [];
for (var _i = rect.left; _i < rect.right; _i++) {
var _colWidth = columnsWidths[_i];
if (_colWidth) {
width += _colWidth;
indexes.push(_i);
}
}
var left = offset + width / 2 - tableDeleteButtonSize / 2;
return {
left: left,
indexes: indexes
};
};
// give a row colspan and a colwidths
// and map to a row's colwidths
var mapTableColwidthsToRow = function mapTableColwidthsToRow(rowColSpans, tableColWidths) {
var curColIdx = 0;
var colWidths = [];
rowColSpans.forEach(function (cellColSpan) {
var colWidth = tableColWidths.slice(curColIdx, curColIdx + cellColSpan).reduce(function (acc, val) {
return acc + val;
}, 0);
curColIdx += cellColSpan;
colWidths.push(colWidth);
});
return colWidths;
};
var getRelativeDomCellWidths = function getRelativeDomCellWidths(_ref) {
var width = _ref.width,
colspan = _ref.colspan,
colwidth = _ref.colwidth;
// For cells with colSpan 1
// or
// for cells in a table with unchanged column widths,
// these are identified by the lack of colwidth data attribute,
// return equally partitioned total cell width in DOM for each cell.
if (colspan === 1 || !colwidth) {
return new Array(colspan).fill(width / colspan);
}
// For cells that have colSpan > 1 and
// are part of a table with resized columns
// return the current total DOM width of the cell multiplied
// by the percentage of the each individual cell's size.
// The cell size percentage is calculated using individual colwidth of the cell,
// from data-colwidth attribute on the cell,
// divided by the total width of the cells from colwidths for merged cells.
// Ex:
// colwidth = ‘201,102’
// Total colWidth = 303
// returned cellWidths = [303 * (201/303), 303 * (102/303)]
// For merged cells we get back colwidth as `201,102`
var cellColWidths = colwidth.split(',').map(function (colwidth) {
return Number(colwidth);
});
var totalCalculatedCellWidth = cellColWidths.reduce(function (acc, cellColWidth) {
return acc + cellColWidth;
}, 0);
return cellColWidths.map(function (cellColWidth) {
return width * (cellColWidth / totalCalculatedCellWidth);
});
};
export var colWidthsForRow = function colWidthsForRow(tr) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref2$useColwidthRati = _ref2.useColwidthRatios,
useColwidthRatios = _ref2$useColwidthRati === void 0 ? false : _ref2$useColwidthRati;
// get the colspans
var rowColSpans = maphElem(tr, function (cell) {
return Number(cell.getAttribute('colspan') || 1 /* default to span of 1 */);
});
// Chrome has trouble aligning borders with auto tables
// and the rest of the page grid. tables with defined
// column widths align fine.
//
// eg a 4x25% table might end up as 189px 190px 190px 190px
//
// prefer copying the widths via the DOM
// or inferring from the next row if one exists
var copyTarget = tr.nextElementSibling || tr;
if (copyTarget) {
// either from the first row while it's still in the table
var cellInfos = maphElem(copyTarget, function (cell) {
return {
width: cell.getBoundingClientRect().width,
colspan: Number(cell.getAttribute('colspan') || 1),
colwidth: cell.dataset.colwidth
};
});
// reverse engineer cell widths from table widths
var domBasedCellWidths = [];
cellInfos.map(function (cell) {
domBasedCellWidths.push.apply(domBasedCellWidths, _toConsumableArray(useColwidthRatios ? getRelativeDomCellWidths(cell) : new Array(cell.colspan).fill(cell.width / cell.colspan)));
});
if (cellInfos.reduce(function (acc, cell) {
return acc + cell.width;
}, 0) !== 0) {
var newWidths = mapTableColwidthsToRow(rowColSpans, domBasedCellWidths);
return newWidths.map(function (px) {
return "".concat(px, "px");
}).join(' ');
}
}
// as a fallback, just calculate a %, and hope that
// it aligns perfectly in the user's browser
var visualColCount = rowColSpans.reduce(function (acc, val) {
return acc + val;
}, 0);
var pctWidths = rowColSpans.map(function (cellColSpan) {
return cellColSpan / visualColCount * 100;
});
return pctWidths.map(function (pct) {
return "".concat(pct, "%");
}).join(' ');
};
/**
* Returns the visual column index that the mouse pointer is over within a column-spanned
* cell, by walking the per-column boundaries of the spanned cell and finding the column whose
* horizontal range contains `mouseEvent.clientX`.
*
* `<td>.cellIndex` for a cell with `colspan > 1` only resolves to the first column the cell
* occupies, so hovering anywhere inside a colspanned first-row cell pins the column drag handle
* to that first column. This resolver disambiguates which visual column the pointer is actually
* over so the handle/menu can target a single column.
*
* The search is restricted to columns in `[startIndex, endIndex)` (the cell's own span). The
* spanned cell's bounding rect is read once and split into per-column boundaries using the cell's
* `data-colwidth` ratios (falling back to an even split for unresized columns) — this keeps the
* work bounded by the colspan size and avoids measuring the whole table.
*
* Returns `undefined` when the mouse is outside the spanned range (so callers can fall back to
* the converted HTML column index).
*/
export var getColumnIndexByMousePosition = function getColumnIndexByMousePosition(cellElement, mouseEvent, columnIndexRange) {
var _cellElement$dataset$;
var startIndex = columnIndexRange.startIndex,
endIndex = columnIndexRange.endIndex;
var columnCount = endIndex - startIndex;
if (columnCount <= 0) {
return undefined;
}
var cellRect = cellElement.getBoundingClientRect();
if (mouseEvent.clientX < cellRect.left || mouseEvent.clientX >= cellRect.right) {
return undefined;
}
// Read DOM ratios here to avoid resolving the ProseMirror node on this mouse-move path.
var ratios = (_cellElement$dataset$ = cellElement.dataset.colwidth) === null || _cellElement$dataset$ === void 0 ? void 0 : _cellElement$dataset$.split(',').map(Number).filter(function (value) {
return !Number.isNaN(value);
});
var perColumnWidths = getProportionalColumnWidths(cellRect.width, columnCount, ratios);
var offsetFromLeft = mouseEvent.clientX - cellRect.left;
var cumulativeWidth = 0;
for (var column = 0; column < columnCount; column++) {
cumulativeWidth += perColumnWidths[column];
if (offsetFromLeft < cumulativeWidth) {
return startIndex + column;
}
}
return endIndex - 1;
};
export var convertHTMLCellIndexToColumnIndex = function convertHTMLCellIndexToColumnIndex(htmlColIndex, htmlRowIndex, tableMap) {
// Same numbers (positions) in tableMap.map array mean that there are merged cells.
// Cells can be merged across columns. So we need to check if the cell on the left and current cell have the same value.
// Cells can be merged acroll rows. So we need to check if the cell above has the same value as current cell.
// When cell has the same value as the cell above it or the cell to the left of it, html cellIndex won't count it a separete column.
var width = tableMap.width;
var map = tableMap.map;
var htmlColCount = 0;
if (htmlRowIndex === 0) {
for (var colIndex = 0; colIndex < width; colIndex++) {
if (colIndex === 0 || map[colIndex] !== map[colIndex - 1]) {
htmlColCount++;
}
if (htmlColCount - 1 === htmlColIndex) {
return colIndex;
}
}
} else {
for (var _colIndex = 0; _colIndex < width; _colIndex++) {
var currentCellMapIndex = htmlRowIndex * width + _colIndex;
var cellAboveMapIndex = (htmlRowIndex - 1) * width + _colIndex;
if (_colIndex > 0) {
if (map[currentCellMapIndex] !== map[currentCellMapIndex - 1] && map[currentCellMapIndex] !== map[cellAboveMapIndex]) {
htmlColCount++;
}
} else {
if (map[currentCellMapIndex] !== map[cellAboveMapIndex]) {
htmlColCount++;
}
}
if (htmlColCount - 1 === htmlColIndex) {
return _colIndex;
}
}
}
return 0;
};
// When first row has merged cells, our converted column index needs to be mapped.
export var getColumnIndexMappedToColumnIndexInFirstRow = function getColumnIndexMappedToColumnIndexInFirstRow(convertedColIndex, htmlRowIndex, tableMap) {
var width = tableMap.width;
var map = tableMap.map;
var mapColIndexToFistRowColIndex = [];
var htmlColCounFirstRow = 0;
var colSpan = 0;
if (htmlRowIndex === 0) {
return convertedColIndex;
}
for (var colIndex = 0; colIndex < width; colIndex++) {
if (colIndex === 0 || map[colIndex] !== map[colIndex - 1]) {
if (colSpan > 0) {
htmlColCounFirstRow += colSpan;
colSpan = 0;
}
htmlColCounFirstRow++;
} else if (map[colIndex] === map[colIndex - 1]) {
colSpan++;
}
mapColIndexToFistRowColIndex[colIndex] = htmlColCounFirstRow - 1;
}
return mapColIndexToFistRowColIndex[convertedColIndex];
};