Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/vue-vtable/demo/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ListTableDes from './table/gramatical/composition/ListTable-destruction.v
import ListTableCustom from './table/gramatical/composition/ListTable-custom.vue';
import ListTableCustomHover from './table/gramatical/composition/ListTable-custom-hover.vue';
import Issue5150CustomLayoutSort from './table/gramatical/composition/Issue5150CustomLayoutSort.vue';
import Issue5157CustomLayoutScrollbar from './table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue';
import ListTableVFor from './table/gramatical/options/ListTable-v-for.vue';

import PivotTable from './table/gramatical/options/PivotTable.vue';
Expand Down Expand Up @@ -51,8 +52,9 @@ import singleRadio from './table/single/single-radio.vue';
<!-- gramatical -->
<!-- ---------- -->

<ListTable />
<!-- <ListTable /> -->
<!-- <Issue5150CustomLayoutSort /> -->
<Issue5157CustomLayoutScrollbar />
<!-- <ListTableEditor /> -->
<!-- <ListTableEditorArco /> -->
<!-- <ListTableEditorRender /> -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<template>
<div class="toolbar">
<button @click="check">Check scrollbar hit area</button>
<span>{{ state }}</span>
</div>
<div class="issue5157-table">
<vue-list-table ref="tableRef" :options="tableOptions">
<ListColumn field="name" title="Name" width="180">
<template #customLayout="{ width, height, record, row }">
<Group
:width="width"
:height="height"
:vue="{
element: renderAction(record, row, 'Frozen'),
pointerEvents: true,
id: `issue5157-frozen-${row}`
}"
/>
</template>
</ListColumn>
<ListColumn field="value" title="Value" width="180" />
<ListColumn field="action" title="Action" width="260">
<template #customLayout="{ width, height, record, row }">
<Group
:width="width"
:height="height"
:vue="{
element: renderAction(record, row, 'Action'),
pointerEvents: true,
id: `issue5157-body-${row}`
}"
/>
</template>
</ListColumn>
</vue-list-table>
</div>
</template>

<script setup lang="ts">
import { h, nextTick, ref } from 'vue';
import { Group, ListColumn } from '../../../../../src/components/index';
import * as VTable from '../../../../../../vtable/src/index';

const tableRef = ref();
const state = ref('READY');

const records = Array.from({ length: 100 }, (_, index) => ({
id: index + 1,
name: `row-${index + 1}`,
value: `value-${index + 1}`,
action: 'clickable'
}));

const tableOptions = ref({
records,
widthMode: 'standard',
heightMode: 'standard',
defaultRowHeight: 44,
defaultHeaderRowHeight: 40,
frozenColCount: 1,
scrollFrozenCols: true,
maxFrozenWidth: 80,
theme: {
...VTable.themes.DEFAULT,
scrollStyle: {
...VTable.themes.DEFAULT.scrollStyle,
visible: 'always',
width: 16
}
},
customConfig: {
createReactContainer: true
}
});

function renderAction(record: any, row: number, label: string) {
return h(
'button',
{
class: 'action-button',
'data-row': row,
onClick: () => {
state.value = `clicked ${record.id}`;
}
},
`${label} ${record.id}`
);
}

function check() {
const table = tableRef.value?.vTableInstance;
const bodyDoms = Array.from(document.querySelectorAll<HTMLElement>('[id^="vue_issue5157-body-"]'));
const frozenDoms = Array.from(document.querySelectorAll<HTMLElement>('[id^="vue_issue5157-frozen-"]'));
if (!table || !bodyDoms.length || !frozenDoms.length) {
state.value = 'FAIL | missing table or dom';
return { pass: false };
}

const firstBodyRect = bodyDoms[0].getBoundingClientRect();
const firstFrozenRect = frozenDoms[0].getBoundingClientRect();
const tableRect = table.getElement().getBoundingClientRect();
const scrollbarSize = table.theme?.scrollStyle?.width ?? 16;
const scrollbarLeft = tableRect.left + table.tableNoFrameWidth - scrollbarSize;
const frozenScrollbarTop = tableRect.top + Math.min(table.tableNoFrameHeight, table.getAllRowsHeight()) - scrollbarSize;
const clippedFrozenDoms = frozenDoms.filter(dom => {
const rect = dom.getBoundingClientRect();
return rect.bottom > frozenScrollbarTop && getComputedStyle(dom).clipPath !== 'none';
});
const pass =
firstBodyRect.right <= scrollbarLeft + 0.5 &&
firstFrozenRect.bottom <= frozenScrollbarTop + 0.5 &&
clippedFrozenDoms.length > 0;
state.value =
`${pass ? 'PASS' : 'FAIL'} | bodyRight=${Math.round(firstBodyRect.right)} scrollbarLeft=${Math.round(scrollbarLeft)} ` +
`frozenBottom=${Math.round(firstFrozenRect.bottom)} frozenScrollbarTop=${Math.round(frozenScrollbarTop)} ` +
`clippedFrozen=${clippedFrozenDoms.length}`;
return {
pass,
bodyRight: firstBodyRect.right,
scrollbarLeft,
frozenBottom: firstFrozenRect.bottom,
frozenScrollbarTop,
clippedFrozenCount: clippedFrozenDoms.length,
frozenOffset: table.getFrozenColsOffset?.()
};
}

nextTick(() => {
setTimeout(check, 0);
});

(window as any).issue5157Check = check;
</script>

<style scoped>
.toolbar {
height: 36px;
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}

.issue5157-table {
width: 620px;
height: 360px;
}

.action-button {
width: 100%;
height: 100%;
border: 0;
background: #e8f3ff;
color: #1664ff;
cursor: pointer;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,16 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
const { vue: options, width, height, visible, display, ...rest } = attribute || {};
const { x: left, y: top } = this.calculatePosition(graphic, options.anchorType);
const { left: offsetX, top: offsetTop } = this.calculateOffset(stage, nativeContainer, left, top);
const { safeWidth, safeHeight } = this.getScrollbarSafeSize(
graphic,
stage,
nativeContainer,
offsetX,
offsetTop,
width,
height,
options
);

const { id } = this.getGraphicOptions(graphic) || {};
const record = id ? this.htmlMap[id] : null;
Expand All @@ -499,22 +509,20 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
// 位置变化检查
const positionChanged =
!record.lastPosition || record.lastPosition.x !== offsetX || record.lastPosition.y !== offsetTop;
if (!positionChanged) {
// 位置没有变化,无需更新样式
return;
}

// 默认自定义区域内也可带动表格画布滚动
const { pointerEvents } = options;
const calculateStyle = this.parseDefaultStyleFromGraphic(graphic);
const clipStyle = this.getScrollbarClipStyle(width, height, safeWidth, safeHeight);
// 单元格样式
const style = this.convertCellStyle(graphic);
Object.assign(calculateStyle, {
width: `${width}px`,
height: `${height}px`,
width: `${safeWidth}px`,
height: `${safeHeight}px`,
overflow: 'hidden',
...(style || {}),
...(rest || {}),
...clipStyle,
transform: `translate(${offsetX}px, ${offsetTop}px)`,
boxSizing: 'border-box',
display: visible !== false ? display || 'block' : 'none',
Expand Down Expand Up @@ -546,6 +554,152 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl

record.lastStyle = calculateStyle;
}
record.lastPosition = positionChanged ? { x: offsetX, y: offsetTop } : record.lastPosition;
}

private getScrollbarSafeSize(
graphic: IGraphic,
stage: IStage,
nativeContainer: HTMLElement,
offsetX: number,
offsetTop: number,
width: number,
height: number,
options: any
) {
const pointerEvents = options?.pointerEvents === true ? 'all' : options?.pointerEvents || 'none';
const table = getTargetGroup(graphic)?.stage?.table;
const scrollStyle = table?.theme?.scrollStyle;
const barToSide = scrollStyle?.barToSide ?? false;
let safeWidth = width;
let safeHeight = height;

if (pointerEvents !== 'none' && table && !barToSide) {
const verticalVisible = scrollStyle?.verticalVisible ?? scrollStyle?.visible;
const horizontalVisible = scrollStyle?.horizontalVisible ?? scrollStyle?.visible;
const sizeTolerance = this.getScrollSizeTolerance(table);
const hasVerticalScrollBar =
verticalVisible !== 'none' && table.getAllRowsHeight() > table.tableNoFrameHeight + sizeTolerance;
const hasHorizontalScrollBar =
horizontalVisible !== 'none' && this.getBodyHorizontalScrollRange(table) > sizeTolerance;
const scrollBarSize = scrollStyle?.width ?? 7;
const groupX = table.scenegraph?.tableGroup?.attribute?.x ?? 0;
const groupY = table.scenegraph?.tableGroup?.attribute?.y ?? 0;
const hoverOn = scrollStyle?.hoverOn;
const domRight = offsetX + width;
const domBottom = offsetTop + height;
const ignoreFrozenCols = scrollStyle?.ignoreFrozenCols ?? false;

if (hasVerticalScrollBar) {
const verticalLeft =
Math.min(table.tableNoFrameWidth, table.getAllColsWidth()) - (hoverOn ? scrollBarSize : -groupX);
const verticalTop = table.getFrozenRowsHeight() + (!hoverOn ? groupY : 0);
const verticalBottom =
verticalTop + table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight();
const { left: verticalDomLeft, top: verticalDomTop } = this.calculateOffset(
stage,
nativeContainer,
verticalLeft,
verticalTop
);
const verticalDomBottom = verticalDomTop + verticalBottom - verticalTop;

if (
offsetTop < verticalDomBottom &&
domBottom > verticalDomTop &&
offsetX < verticalDomLeft + scrollBarSize &&
domRight > verticalDomLeft
) {
safeWidth = Math.max(0, Math.min(width, verticalDomLeft - offsetX));
}
}

const clipHorizontalScrollbar = (horizontalLeft: number, horizontalWidth: number) => {
const horizontalTop =
Math.min(table.tableNoFrameHeight, table.getAllRowsHeight()) - (hoverOn ? scrollBarSize : -groupY);
const horizontalRight = horizontalLeft + horizontalWidth;
const { left: horizontalDomLeft, top: horizontalDomTop } = this.calculateOffset(
stage,
nativeContainer,
horizontalLeft,
horizontalTop
);
const horizontalDomRight = horizontalDomLeft + horizontalRight - horizontalLeft;

if (
offsetX < horizontalDomRight &&
domRight > horizontalDomLeft &&
offsetTop < horizontalDomTop + scrollBarSize &&
domBottom > horizontalDomTop
) {
safeHeight = Math.max(0, Math.min(height, horizontalDomTop - offsetTop));
}
};

if (hasHorizontalScrollBar) {
const horizontalLeft = ignoreFrozenCols
? !hoverOn
? groupX
: 0
: table.getFrozenColsWidth() + (!hoverOn ? groupX : 0);
const horizontalWidth = ignoreFrozenCols
? table.tableNoFrameWidth
: table.tableNoFrameWidth - table.getFrozenColsWidth() - table.getRightFrozenColsWidth();
clipHorizontalScrollbar(horizontalLeft, horizontalWidth);
}

if (
horizontalVisible !== 'none' &&
!ignoreFrozenCols &&
table.options?.scrollFrozenCols &&
table.getFrozenColsOffset?.() > 0
) {
clipHorizontalScrollbar(!hoverOn ? groupX : 0, table.getFrozenColsWidth());
}

if (
horizontalVisible !== 'none' &&
!ignoreFrozenCols &&
table.options?.scrollRightFrozenCols &&
table.getRightFrozenColsOffset?.() > 0
) {
clipHorizontalScrollbar(
table.tableNoFrameWidth - table.getRightFrozenColsWidth() + (!hoverOn ? groupX : 0),
table.getRightFrozenColsWidth()
);
}
}

return { safeWidth, safeHeight };
}

private getScrollbarClipStyle(width: number, height: number, safeWidth: number, safeHeight: number) {
if (safeWidth >= width && safeHeight >= height) {
return { clipPath: 'none' };
}

const right = safeWidth < width ? `calc(100% - ${safeWidth}px)` : '0px';
const bottom = safeHeight < height ? `calc(100% - ${safeHeight}px)` : '0px';

return {
clipPath: `inset(0 ${right} ${bottom} 0)`
};
}

private getBodyHorizontalScrollRange(table: any) {
const totalWidth = table.getAllColsWidth();
const frozenColsWidth = table.getFrozenColsWidth();
const rightFrozenColsWidth = table.getRightFrozenColsWidth();
const frozenColsContentWidth = table.getFrozenColsContentWidth?.() ?? frozenColsWidth;
const rightFrozenColsContentWidth = table.getRightFrozenColsContentWidth?.() ?? rightFrozenColsWidth;
const bodyViewportWidth = table.tableNoFrameWidth - frozenColsWidth - rightFrozenColsWidth;
const bodyContentWidth = totalWidth - frozenColsContentWidth - rightFrozenColsContentWidth;

return Math.max(0, bodyContentWidth - bodyViewportWidth);
}

private getScrollSizeTolerance(table: any) {
return table.options?.customConfig?._disableColumnAndRowSizeRound ? 1 : 0;
}

/**
Expand Down
Loading