Merge pull request #2766 from VisActor/2026-multiple-rowSelected-highlight
Some checks are pending
Unit test CI / build (18.x) (push) Waiting to run

[WIP] 2026 multiple row selected highlight
This commit is contained in:
方帅 2024-11-13 14:41:09 +08:00 committed by GitHub
commit e6acbdce1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 646 additions and 32 deletions

View File

@ -32,5 +32,6 @@
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
"python.formatting.provider": "none",
"nuxt.isNuxtApp": false
}

View File

@ -20,4 +20,4 @@
#
registry=https://registry.npmjs.org/
always-auth=false
always-auth=false

View File

@ -245,6 +245,10 @@ Whether to cancel the selection when clicking outside the table.
Whether to disable dragging selection.
##${prefix} highlightInRange(boolean) = false
Will the entire row or column be highlighted when select in multiple rows or columns?
#${prefix} theme(Object)
{{ use: common-theme(

View File

@ -240,6 +240,10 @@ hover 交互响应模式:十字交叉、整列、整行或者单个单元格
拖拽选择单元格时是否禁用框选。
##${prefix} highlightInRange(boolean) = false
是否在多行或者多列时展示整行或整列高亮效果。
#${prefix} theme(Object)
{{ use: common-theme(

View File

@ -0,0 +1,267 @@
// @ts-nocheck
// 有问题可对照demo unitTestListTable
import records from './data/marketsales.json';
import { ListTable } from '../src';
import { createDiv } from './dom';
global.__VERSION__ = 'none';
describe('listTable init test', () => {
const containerDom: HTMLElement = createDiv();
containerDom.style.position = 'relative';
containerDom.style.width = '1000px';
containerDom.style.height = '800px';
const columns = [
{
field: '订单 ID',
caption: '订单 ID',
sort: true,
width: 'auto',
description: '这是订单的描述信息',
style: {
fontFamily: 'Arial',
fontSize: 14
}
},
{
field: '订单日期',
caption: '订单日期'
},
{
field: '发货日期',
caption: '发货日期'
},
{
field: '客户名称',
caption: '客户名称',
style: {
padding: [10, 0, 10, 60]
}
},
{
field: '邮寄方式',
caption: '邮寄方式'
},
{
field: '省/自治区',
caption: '省/自治区'
},
{
field: '产品名称',
caption: '产品名称'
},
{
field: '类别',
caption: '类别'
},
{
field: '子类别',
caption: '子类别'
},
{
field: '销售额',
caption: '销售额'
},
{
field: '数量',
caption: '数量'
},
{
field: '折扣',
caption: '折扣'
},
{
field: '利润',
caption: '利润'
}
];
const option = {
columns,
defaultColWidth: 150,
allowFrozenColCount: 5,
select: {
highlightInRange: true
}
};
option.container = containerDom;
option.records = records;
const listTable = new ListTable(option);
test('listTable getCellValue', () => {
expect(listTable.getCellValue(6, 3)).toBe('Cardinal 孔加固材料, 回收');
});
test('listTable getCellOverflowText', () => {
expect(listTable.getCellOverflowText(6, 3)).toBe('Cardinal 孔加固材料, 回收');
});
test('listTable getHeaderDescription', () => {
expect(listTable.getHeaderDescription(0, 0)).toBe('这是订单的描述信息');
});
test('listTable setScrollTop getScrollTop', () => {
listTable.setScrollTop(100);
expect(listTable.getScrollTop()).toBe(100);
});
test('listTable setScrollLeft getScrollLeft', () => {
listTable.setScrollLeft(100);
expect(listTable.getScrollLeft()).toBe(100);
});
test('listTable scrollToCell', () => {
listTable.scrollToCell({ col: 4, row: 28 });
expect(listTable.getScrollLeft()).toBe(601);
expect(listTable.getScrollTop()).toBe(802);
});
test('listTable updateTheme', () => {
listTable.heightMode = 'autoHeight';
listTable.updateTheme({
bodyStyle: {
fontFamily: 'Calibri',
fontSize: 28,
color: 'red'
}
});
listTable.scrollToCell({ col: 6, row: 16 });
expect(listTable.getScrollLeft()).toBe(901);
expect(listTable.getScrollTop()).toBe(720);
expect(listTable.getCellStyle(6, 16)).toStrictEqual({
strokeColor: undefined,
textAlign: 'left',
textBaseline: 'middle',
bgColor: '#FFF',
color: 'red',
fontFamily: 'Calibri',
fontSize: 28,
fontStyle: undefined,
fontVariant: undefined,
fontWeight: undefined,
lineHeight: 28,
autoWrapText: false,
lineClamp: 'auto',
textOverflow: 'ellipsis',
borderColor: '#000',
borderLineWidth: 1,
borderLineDash: [],
underline: false,
underlineDash: undefined,
underlineOffset: undefined,
underlineWidth: undefined,
lineThrough: false,
lineThroughLineWidth: undefined,
padding: [10, 16, 10, 16],
_linkColor: '#3772ff',
_strokeArrayColor: undefined,
_strokeArrayWidth: undefined
});
});
test('listTable updateOption records&autoWidth&widthMode', () => {
columns.shift();
const recordDeleted = records.slice(10, 30);
const option1 = {
columns,
records: recordDeleted,
defaultColWidth: 150,
allowFrozenColCount: 5,
heightMode: 'autoHeight',
autoWrapText: true,
widthMode: 'autoWidth',
limitMaxAutoWidth: 170,
dragHeaderMode: 'all'
};
listTable.updateOption(option1);
expect(listTable.rowCount).toBe(21);
expect(listTable.colCount).toBe(12);
expect(listTable.getScrollTop()).toBe(0);
expect(listTable.getColWidth(0)).toBe(122);
expect(listTable.getColWidth(5)).toBe(170);
});
test('listTable selectCell', () => {
listTable.selectCell(4, 5);
expect(listTable.stateManager?.select.ranges).toEqual([
{
start: {
col: 4,
row: 5
},
end: {
col: 4,
row: 5
}
}
]);
const scrollTop = listTable.scrollTop;
listTable.selectCells([
{ start: { col: 1, row: 3 }, end: { col: 4, row: 6 } },
{ start: { col: 0, row: 4 }, end: { col: 7, row: 4 } },
{ start: { col: 4, row: 36 }, end: { col: 7, row: 36 } }
]);
expect(listTable.stateManager?.select.ranges).toEqual([
{ start: { col: 1, row: 3 }, end: { col: 4, row: 6 }, skipBodyMerge: true },
{ start: { col: 0, row: 4 }, end: { col: 7, row: 4 }, skipBodyMerge: true },
{ start: { col: 4, row: 36 }, end: { col: 7, row: 36 }, skipBodyMerge: true }
]);
expect(listTable.getScrollTop()).toBe(scrollTop);
});
test('listTable measureTextWidth', () => {
const measureTextWdith = listTable.measureText("家里方大化工撒个福建师大看哈 fdsfgj! *-+&5%#.,'.,。、", {
fontFamily: 'Arial',
fontSize: 15,
fontWeight: 'bold'
});
expect(measureTextWdith).toEqual({
width: 390.0501427283655,
height: 15
});
});
// test('listTable API getAllCells', () => {
// expect(JSON.parse(JSON.stringify(listTable.getCellInfo(5, 5)))).toEqual({
// col: 5,
// row: 5,
// field: '省/自治区',
// cellHeaderPaths: {
// colHeaderPaths: [
// {
// field: '省/自治区'
// }
// ],
// rowHeaderPaths: []
// },
// caption: '省/自治区',
// cellType: 'text',
// originData: {
// '行 ID': '5',
// '订单 ID': 'CN-2018-2975416',
// 订单日期: '2018/5/31',
// 发货日期: '2018/6/2',
// 邮寄方式: '二级',
// '客户 ID': '万兰-15730',
// 客户名称: '万兰',
// 细分: '消费者',
// 城市: '汕头',
// '省/自治区': '广东',
// '国家/地区': '中国',
// 地区: '中南',
// '产品 ID': '办公用-器具-10003452',
// 类别: '办公用品',
// 子类别: '器具',
// 产品名称: 'KitchenAid 搅拌机, 黑色',
// 销售额: '1375.92',
// 数量: '3',
// 折扣: '0',
// 利润: '550.2'
// },
// cellRange: {
// bounds: {
// x1: 762,
// y1: 200,
// x2: 912,
// y2: 240
// }
// },
// value: '广东',
// dataValue: '广东',
// cellType: 'body',
// scaleRatio: 1
// });
// });
// setTimeout(() => {
// listTable.release();
// }, 1000);
});

View File

@ -0,0 +1,274 @@
import * as VTable from '../../src';
import { bindDebugTool } from '../../src/scenegraph/debug-tool';
const CONTAINER_ID = 'vTable';
const generatePersons = count => {
return Array.from(new Array(count)).map((_, i) => ({
id: i + 1,
email1: `${i + 1}@xxx.com`,
name: `小明${i + 1}`,
lastName: '王',
date1: '2022年9月1日',
tel: '000-0000-0000',
sex: i % 2 === 0 ? 'boy' : 'girl',
work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1),
city: 'beijing'
}));
};
VTable.register.icon('sort_normal', {
type: 'svg',
svg: `<svg t="1669210412838" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5700" width="200" height="200"><path d="M420.559974 72.98601l-54.855 0 0 774.336c-52.455014-69.163008-121.619046-123.762995-201.120051-157.052006l0 61.968c85.838029 41.401958 156.537958 111.337984 201.120051 198.221005l0 0.208 54.855 0 0-13.047c0.005018-0.00297 0.010035-0.005018 0.01495-0.007987-0.005018-0.010035-0.010035-0.019968-0.01495-0.030003L420.559974 72.986zM658.264986 73.385984l0-0.4L603.41 72.985984l0 877.68 54.855 0L658.265 176.524c52.457984 69.178982 121.632051 123.790029 201.149952 157.078016l0-61.961C773.560013 230.238003 702.853018 160.287027 658.264986 73.385984z" p-id="5701"></path></svg>`,
width: 20, //其实指定的是svg图片绘制多大实际占位是boxmargin也是相对阴影范围指定的
height: 20,
funcType: VTable.TYPES.IconFuncTypeEnum.sort,
name: 'sort_normal',
positionType: VTable.TYPES.IconPosition.inlineFront,
marginLeft: 0,
marginRight: 0,
hover: {
width: 24,
height: 24,
bgColor: 'rgba(22,44,66,0.5)'
},
cursor: 'pointer'
});
export function createTable() {
const records = generatePersons(2000);
const columns: VTable.ColumnsDefine = [
{
field: '',
title: '行号',
width: 80,
fieldFormat(data, col, row, table) {
return row - 1;
},
style: {
underline: true,
underlineDash: [2, 0],
underlineOffset: 3
}
},
{
field: 'id',
title: 'ID',
width: 'auto',
minWidth: 50,
sort: true
},
{
field: 'email1',
title: 'email',
width: 200,
sort: true,
style: {
underline: true,
underlineDash: [2, 0],
underlineOffset: 3
}
},
// {
// title: 'full name',
// columns: [
// {
// field: 'name',
// title: 'First Name',
// width: 200
// },
// {
// field: 'name',
// title: 'Last Name',
// width: 200
// }
// ]
// },
{
field: 'date1',
title: 'birthday',
width: 200
},
{
field: 'sex',
title: 'sex',
width: 100
},
{
field: 'tel',
title: 'telephone',
width: 150
},
{
field: 'work',
title: 'job',
width: 200
},
{
field: 'city',
title: 'city',
width: 150
},
{
field: 'date1',
title: 'birthday',
width: 200
},
{
field: 'sex',
title: 'sex',
width: 100
},
{
field: 'tel',
title: 'telephone',
width: 150
},
{
field: 'work',
title: 'job',
width: 200
},
{
field: 'city',
title: 'city',
width: 150
},
{
field: 'date1',
title: 'birthday',
width: 200
},
{
field: 'sex',
title: 'sex',
width: 100
},
{
field: 'tel',
title: 'telephone',
width: 150
},
{
field: 'work',
title: 'job',
width: 200
},
{
field: 'city',
title: 'city',
width: 150
},
{
field: 'date1',
title: 'birthday',
width: 200
},
{
field: 'sex',
title: 'sex',
width: 100
},
{
field: 'tel',
title: 'telephone',
width: 150
},
{
field: 'work',
title: 'job',
width: 200
},
{
field: 'city',
title: 'city',
width: 150
}
];
const option: VTable.ListTableConstructorOptions = {
container: document.getElementById(CONTAINER_ID),
emptyTip: true,
records,
columns: [
...columns
// ...columns,
// ...columns,
// ...columns,
// ...columns,
// ...columns,
// ...columns,
// ...columns,
// ...columns,
// ...columns
],
tooltip: {
isShowOverflowTextTooltip: true
},
frozenColCount: 1,
bottomFrozenRowCount: 2,
rightFrozenColCount: 2,
overscrollBehavior: 'none',
// dragHeaderMode: 'all',
keyboardOptions: {
pasteValueToCell: true,
copySelected: true,
selectAllOnCtrlA: true
},
eventOptions: {
preventDefaultContextMenu: false
},
autoWrapText: true,
editor: '',
// theme: VTable.themes.ARCO,
// hover: {
// highlightMode: 'cross'
// },
// select: {
// headerSelectMode: 'cell',
// highlightMode: 'cross'
// },
theme: {
frameStyle: {
cornerRadius: [10, 0, 0, 10],
// cornerRadius: 10,
borderLineWidth: [10, 0, 10, 10],
// borderLineWidth: 10,
borderColor: 'red',
shadowBlur: 0
},
bodyStyle: {
select: {
cellBgColor: 'red',
inlineRowBgColor: 'pink',
inlineColumnBgColor: 'purple'
}
}
},
// transpose: true,
select: {
headerSelectMode: 'inline',
highlightMode: 'cross',
highlightInRange: true
}
// excelOptions: {
// fillHandle: true
// }
// widthMode: 'adaptive'
};
const tableInstance = new VTable.ListTable(option);
window.tableInstance = tableInstance;
bindDebugTool(tableInstance.scenegraph.stage, {
customGrapicKeys: ['col', 'row']
});
// tableInstance.on('sort_click', args => {
// tableInstance.updateSortState(
// {
// field: args.field,
// order: Date.now() % 3 === 0 ? 'desc' : Date.now() % 3 === 1 ? 'asc' : 'normal'
// },
// false
// );
// return false; //return false代表不执行内部排序逻辑
// });
}

View File

@ -35,6 +35,10 @@ export const menus = [
path: 'list',
name: 'list'
},
{
path: 'list',
name: 'list-highlightInRange'
},
{
path: 'list',
name: 'list-transpose'

View File

@ -2,10 +2,11 @@ import type { StateManager } from '../state';
import type { Group } from '../../scenegraph/graphic/group';
import { getProp } from '../../scenegraph/utils/get-prop';
import type { BaseTableAPI } from '../../ts-types/base-table';
import type { ColumnDefine } from '../../ts-types';
import type { CellRange, ColumnDefine } from '../../ts-types';
import { HighlightScope } from '../../ts-types';
import { isValid } from '@visactor/vutils';
import { getCellMergeRange } from '../../tools/merge-range';
import { cellInRange } from '../../tools/helper';
export function getCellSelectColor(cellGroup: Group, table: BaseTableAPI): string | undefined {
let colorKey;
@ -56,11 +57,84 @@ export function getCellSelectColor(cellGroup: Group, table: BaseTableAPI): strin
return fillColor;
}
export function isCellSelected(state: StateManager, col: number, row: number, cellGroup: Group): string | undefined {
const { highlightScope, disableHeader, cellPos, ranges } = state.select;
// 选中多列
function isSelectMultipleRange(range: CellRange) {
return range.start.col !== range.end.col || range.start.row !== range.end.row;
}
function getSelectModeRange(state: StateManager, col: number, row: number) {
let selectMode;
if (ranges?.length === 1 && ranges[0].end.col === ranges[0].start.col && ranges[0].end.row === ranges[0].start.row) {
const { highlightScope, cellPos, ranges } = state.select;
const range = ranges[0];
const rangeColStart = Math.min(range.start.col, range.end.col);
const rangeColEnd = Math.max(range.start.col, range.end.col);
const rangeRowStart = Math.min(range.start.row, range.end.row);
const rangeRowEnd = Math.max(range.start.row, range.end.row);
if (highlightScope === HighlightScope.single && cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else if (highlightScope === HighlightScope.column && col >= rangeColStart && col <= rangeColEnd) {
if (cellInRange(ranges[0], col, row)) {
selectMode = 'cellBgColor';
} else {
selectMode = 'inlineColumnBgColor';
}
} else if (highlightScope === HighlightScope.row && row >= rangeRowStart && row <= rangeRowEnd) {
if (cellInRange(ranges[0], col, row)) {
selectMode = 'cellBgColor';
} else {
selectMode = 'inlineRowBgColor';
}
} else if (highlightScope === HighlightScope.cross) {
if (cellInRange(ranges[0], col, row)) {
selectMode = 'cellBgColor';
} else if (col >= rangeColStart && col <= rangeColEnd) {
selectMode = 'inlineColumnBgColor';
} else if (row >= rangeRowStart && row <= rangeRowEnd) {
selectMode = 'inlineRowBgColor';
}
}
return selectMode;
}
function getSelectMode(state: StateManager, col: number, row: number) {
let selectMode;
const { highlightScope, cellPos } = state.select;
if (highlightScope === HighlightScope.single && cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else if (highlightScope === HighlightScope.column && cellPos.col === col) {
if (cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else {
selectMode = 'inlineColumnBgColor';
}
} else if (highlightScope === HighlightScope.row && cellPos.row === row) {
if (cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else {
selectMode = 'inlineRowBgColor';
}
} else if (highlightScope === HighlightScope.cross) {
if (cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else if (cellPos.col === col) {
selectMode = 'inlineColumnBgColor';
} else if (cellPos.row === row) {
selectMode = 'inlineRowBgColor';
}
}
return selectMode;
}
export function isCellSelected(state: StateManager, col: number, row: number, cellGroup: Group): string | undefined {
const { highlightInRange, disableHeader, ranges } = state.select;
let selectMode;
const isSelectRange = ranges.length === 1 && isSelectMultipleRange(ranges?.[0]) && highlightInRange;
if (
isSelectRange
? ranges?.length === 1 && ranges[0].start && ranges[0].end
: ranges?.length === 1 && ranges[0].end.col === ranges[0].start.col && ranges[0].end.row === ranges[0].start.row
) {
const table = state.table;
const isHeader = table.isHeader(col, row);
@ -68,29 +142,7 @@ export function isCellSelected(state: StateManager, col: number, row: number, ce
return undefined;
}
if (highlightScope === HighlightScope.single && cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else if (highlightScope === HighlightScope.column && cellPos.col === col) {
if (cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else {
selectMode = 'inlineColumnBgColor';
}
} else if (highlightScope === HighlightScope.row && cellPos.row === row) {
if (cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else {
selectMode = 'inlineRowBgColor';
}
} else if (highlightScope === HighlightScope.cross) {
if (cellPos.col === col && cellPos.row === row) {
selectMode = 'cellBgColor';
} else if (cellPos.col === col) {
selectMode = 'inlineColumnBgColor';
} else if (cellPos.row === row) {
selectMode = 'inlineRowBgColor';
}
}
selectMode = isSelectRange ? getSelectModeRange(state, col, row) : getSelectMode(state, col, row);
if (selectMode) {
let cellDisable;

View File

@ -79,6 +79,7 @@ export class StateManager {
* 'body': body body
*/
headerSelectMode?: 'inline' | 'cell' | 'body';
highlightInRange?: boolean;
selecting: boolean;
};
fillHandle: {
@ -420,7 +421,8 @@ export class StateManager {
headerSelectMode,
disableSelect,
disableHeaderSelect,
highlightMode
highlightMode,
highlightInRange
} = Object.assign(
{},
{
@ -428,7 +430,8 @@ export class StateManager {
headerSelectMode: 'inline',
disableSelect: false,
disableHeaderSelect: false,
highlightMode: 'cell'
highlightMode: 'cell',
highlightInRange: false
},
this.table.options.select
);
@ -457,6 +460,7 @@ export class StateManager {
this.select.singleStyle = !disableSelect;
this.select.disableHeader = disableHeaderSelect;
this.select.headerSelectMode = headerSelectMode;
this.select.highlightInRange = highlightInRange;
}
isSelected(col: number, row: number): boolean {

View File

@ -258,7 +258,9 @@ export { isNode, getChainSafe, applyChainSafe, getOrApply, getIgnoreCase, array
export function cellInRange(range: CellRange, col: number, row: number): boolean {
return (
(range.start.col <= col && col <= range.end.col && range.start.row <= row && row <= range.end.row) ||
(range.end.col <= col && col <= range.start.col && range.end.row <= row && row <= range.start.row)
(range.end.col <= col && col <= range.start.col && range.end.row <= row && row <= range.start.row) ||
(range.end.col <= col && col <= range.start.col && range.start.row <= row && row <= range.end.row) ||
(range.start.col <= col && col <= range.end.col && range.end.row <= row && row <= range.start.row)
);
}
export function cellInRanges(ranges: CellRange[], col: number, row: number): boolean {

View File

@ -375,6 +375,8 @@ export interface BaseTableConstructorOptions {
outsideClickDeselect?: boolean; //
/** 禁止拖拽框选 */
disableDragSelect?: boolean;
/** 是否在选择多行或多列时高亮范围 */
highlightInRange?: boolean;
};
/** 下拉菜单的相关配置。消失时机:显示后点击菜单区域外自动消失*/
menu?: {