Skip to content

Commit

Permalink
feat: customize column header
Browse files Browse the repository at this point in the history
  • Loading branch information
lumixraku committed May 29, 2024
1 parent 5c6b010 commit fd2ee17
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { Nullable } from '@univerjs/core';
import type { IViewportInfo, Vector2 } from '../../basics/vector2';
import type { UniverRenderingContext } from '../../context';
import { SheetColumnHeaderExtensionRegistry } from '../extension';
import type { ColumnHeaderLayout } from './extensions/column-header-layout';
import type { ColumnHeaderLayout, IColumnsHeaderCfgParam } from './extensions/column-header-layout';
import { SpreadsheetHeader } from './sheet-component';
import type { SpreadsheetSkeleton } from './sheet-skeleton';

Expand Down Expand Up @@ -98,4 +98,9 @@ export class SpreadsheetColumnHeader extends SpreadsheetHeader {
'DefaultColumnHeaderLayoutExtension'
) as ColumnHeaderLayout;
}

setCustomHeader(cfg: IColumnsHeaderCfgParam) {
this.makeDirty(true);
this._columnHeaderLayoutExtension.configHeaderColumn(cfg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,76 @@ import { getColor } from '../../../basics/tools';
import type { UniverRenderingContext } from '../../../context';
import { SheetColumnHeaderExtensionRegistry } from '../../extension';
import type { SpreadsheetSkeleton } from '../sheet-skeleton';
import type { IAColumnCfg, IAColumnCfgObj, IColumnStyleCfg } from '../interfaces';
import { SheetExtension } from './sheet-extension';

const UNIQUE_KEY = 'DefaultColumnHeaderLayoutExtension';

export interface IColumnsHeaderCfgParam {
headerStyle?: Partial<IColumnStyleCfg>;
columnsCfg?: IAColumnCfg[];
}
const DEFAULT_COLUMN_STYLE = {
fontSize: 13,
fontFamily: DEFAULT_FONTFACE_PLANE,
fontColor: '#000000',
backgroundColor: getColor([248, 249, 250]),
borderColor: getColor([217, 217, 217]),
textAlign: 'center',
textBaseline: 'middle',
} as const;
export class ColumnHeaderLayout extends SheetExtension {
override uKey = UNIQUE_KEY;

override Z_INDEX = 10;
columnsCfg: IAColumnCfg[] = [];
headerStyle: IColumnStyleCfg = {
fontSize: DEFAULT_COLUMN_STYLE.fontSize,
fontFamily: DEFAULT_COLUMN_STYLE.fontFamily,
fontColor: DEFAULT_COLUMN_STYLE.fontColor,
backgroundColor: DEFAULT_COLUMN_STYLE.backgroundColor,
borderColor: DEFAULT_COLUMN_STYLE.borderColor,
textAlign: DEFAULT_COLUMN_STYLE.textAlign,
textBaseline: DEFAULT_COLUMN_STYLE.textBaseline,
};

constructor(cfg?: IColumnsHeaderCfgParam) {
super();
if (cfg) {
this.configHeaderColumn(cfg);
}
}

configHeaderColumn(cfg: IColumnsHeaderCfgParam) {
this.columnsCfg = cfg.columnsCfg || [];
this.headerStyle = { ...this.headerStyle, ...cfg.headerStyle };
}

getCfgOfCurrentColumn(colIndex: number) {
let mergeWithSpecCfg;
let curColSpecCfg;
const columnsCfg = this.columnsCfg || [];

if (columnsCfg[colIndex]) {
if (typeof columnsCfg[colIndex] == 'string') {
columnsCfg[colIndex] = { text: columnsCfg[colIndex] } as IAColumnCfgObj;
}
curColSpecCfg = columnsCfg[colIndex] as IColumnStyleCfg & { text: string };
mergeWithSpecCfg = { ...this.headerStyle, ...curColSpecCfg };
} else {
mergeWithSpecCfg = { ...this.headerStyle, text: numberToABC(colIndex) };
}
const specStyle = Object.keys(curColSpecCfg || {}).length > 1; // if cfg have more keys than 'text', means there would be special style config for this column.
return [mergeWithSpecCfg, specStyle] as [IAColumnCfgObj, boolean];
}

setStyleToCtx(ctx: UniverRenderingContext, columnStyle: Partial<IColumnStyleCfg>) {
if (columnStyle.textAlign) ctx.textAlign = columnStyle.textAlign;
if (columnStyle.textBaseline) ctx.textBaseline = columnStyle.textBaseline;
if (columnStyle.fontColor) ctx.fillStyle = columnStyle.fontColor;
if (columnStyle.borderColor) ctx.strokeStyle = columnStyle.borderColor;
if (columnStyle.fontSize) ctx.font = `${columnStyle.fontSize}px ${DEFAULT_FONTFACE_PLANE}`;
}

// eslint-disable-next-line max-lines-per-function
override draw(ctx: UniverRenderingContext, parentScale: IScale, spreadsheetSkeleton: SpreadsheetSkeleton) {
const { rowColumnSegment, columnHeaderHeight = 0 } = spreadsheetSkeleton;
const { startColumn, endColumn } = rowColumnSegment;
Expand All @@ -39,8 +100,7 @@ export class ColumnHeaderLayout extends SheetExtension {
return;
}

const { rowHeightAccumulation, columnTotalWidth, columnWidthAccumulation, rowTotalHeight } =
spreadsheetSkeleton;
const { rowHeightAccumulation, columnTotalWidth, columnWidthAccumulation, rowTotalHeight } = spreadsheetSkeleton;

if (
!rowHeightAccumulation ||
Expand All @@ -52,46 +112,76 @@ export class ColumnHeaderLayout extends SheetExtension {
}

const scale = this._getScale(parentScale);
this.setStyleToCtx(ctx, this.headerStyle);

// painting background
ctx.fillStyle = getColor([248, 249, 250])!;
// background
ctx.save();
ctx.fillStyle = this.headerStyle.backgroundColor;
ctx.fillRectByPrecision(0, 0, columnTotalWidth, columnHeaderHeight);
ctx.restore();

ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = getColor([0, 0, 0])!;
ctx.beginPath();
ctx.setLineWidthByPrecision(1);

ctx.translateWithPrecisionRatio(FIX_ONE_PIXEL_BLUR_OFFSET, FIX_ONE_PIXEL_BLUR_OFFSET);

ctx.strokeStyle = getColor([217, 217, 217])!;
ctx.font = `13px ${DEFAULT_FONTFACE_PLANE}`;
let preColumnPosition = 0;
const columnWidthAccumulationLength = columnWidthAccumulation.length;
for (let c = startColumn - 1; c <= endColumn; c++) {
if (c < 0 || c > columnWidthAccumulationLength - 1) {
if (c < 0 || c > columnWidthAccumulation.length - 1) {
continue;
}

const columnEndPosition = columnWidthAccumulation[c];
if (preColumnPosition === columnEndPosition) {
// Skip hidden columns
continue;
continue;// Skip hidden columns
}
const cellBound = { left: preColumnPosition, top: 0, right: columnEndPosition, bottom: columnHeaderHeight, width: columnEndPosition - preColumnPosition, height: columnHeaderHeight };
const [curColumnCfg, specStyle] = this.getCfgOfCurrentColumn(c);

// background
if (specStyle && curColumnCfg.backgroundColor) {
ctx.save();
ctx.fillStyle = curColumnCfg.backgroundColor;
ctx.fillRectByPrecision(cellBound.left, cellBound.top, cellBound.width, cellBound.height);
ctx.restore();
}

// painting line border
ctx.moveToByPrecision(columnEndPosition, 0);
ctx.lineToByPrecision(columnEndPosition, columnHeaderHeight);
// vertical line border
ctx.beginPath();
ctx.moveToByPrecision(cellBound.right, 0);
ctx.lineToByPrecision(cellBound.right, cellBound.height);
ctx.stroke();
// column header text
const textX = (() => {
switch (curColumnCfg.textAlign) {
case 'center':
return cellBound.left + (cellBound.right - cellBound.left) / 2;
case 'right':
return cellBound.right - MIDDLE_CELL_POS_MAGIC_NUMBER;
case 'left':
return cellBound.left + MIDDLE_CELL_POS_MAGIC_NUMBER;
default: // center
return cellBound.left + (cellBound.right - cellBound.left) / 2;
}
})();
const middleYCellRect = cellBound.height / 2 + MIDDLE_CELL_POS_MAGIC_NUMBER; // Magic number 1, because the vertical alignment appears to be off by 1 pixel

if (specStyle) {
ctx.save();
ctx.beginPath();
this.setStyleToCtx(ctx, curColumnCfg);
ctx.rectByPrecision(cellBound.left, cellBound.top, cellBound.width, cellBound.height);
ctx.clip();
}

ctx.fillText(curColumnCfg.text, textX, middleYCellRect);
if (specStyle) {
ctx.restore();
}

// painting column header text
const middleCellPos = preColumnPosition + (columnEndPosition - preColumnPosition) / 2;
ctx.fillText(numberToABC(c), middleCellPos, columnHeaderHeight / 2 + MIDDLE_CELL_POS_MAGIC_NUMBER); // Magic number 1, because the vertical alignment appears to be off by 1 pixel
preColumnPosition = columnEndPosition;
}

// painting line bottom border
// border bottom line
const columnHeaderHeightFix = columnHeaderHeight - 0.5 / scale;
ctx.beginPath();
ctx.moveToByPrecision(0, columnHeaderHeightFix);
ctx.lineToByPrecision(columnTotalWidth, columnHeaderHeightFix);
ctx.stroke();
Expand Down
12 changes: 12 additions & 0 deletions packages/engine-render/src/components/sheets/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,15 @@ export interface IPaintForScrolling {
scaleX: number;
scaleY: number;
}
export interface IColumnStyleCfg {
fontFamily: string;
fontColor: string;
fontSize: number;
borderColor: string;
textAlign: CanvasTextAlign;
textBaseline: CanvasTextBaseline;
backgroundColor: string;
}

export type IAColumnCfgObj = IColumnStyleCfg & { text: string };
export type IAColumnCfg = undefined | null | string | Partial<IAColumnCfgObj>;
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ export class SpreadsheetSkeleton extends Skeleton {
return this._columnHeaderHeight;
}

/**
* row col start & end range
*/
get rowColumnSegment() {
return this._rowColumnSegment;
}
Expand Down
32 changes: 30 additions & 2 deletions packages/facade/src/apis/__tests__/facade.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
* limitations under the License.
*/

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ICellData, IStyleData, Nullable } from '@univerjs/core';
import { ICommandService, IUniverInstanceService } from '@univerjs/core';
import { SetRangeValuesCommand, SetRangeValuesMutation, SetStyleCommand } from '@univerjs/sheets';
import type { Injector } from '@wendellhu/redi';

import type { RenderComponentType, SheetComponent } from '@univerjs/engine-render';
import type { ColumnHeaderLayout, RenderComponentType, SheetComponent, SpreadsheetColumnHeader } from '@univerjs/engine-render';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SHEET_VIEW_KEY } from '@univerjs/sheets-ui';
import { RegisterFunctionMutation, SetFormulaCalculationStartMutation, UnregisterFunctionMutation } from '@univerjs/engine-formula';
Expand Down Expand Up @@ -102,6 +102,16 @@ describe('Test FUniver', () => {

return renderComponent;
};

vi.useFakeTimers();
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
return setTimeout(callback, 16); // 假设一帧大约16毫秒
});
});

afterEach(() => {
(window.requestAnimationFrame as any).mockRestore();
vi.useRealTimers();
});

it('Function onBeforeCommandExecute', () => {
Expand Down Expand Up @@ -267,4 +277,22 @@ describe('Test FUniver', () => {
},
]);
});

it('Function customizeColumnHeader', () => {
const unitId = univerAPI.getActiveWorkbook()?.getId() || '';
const columnRenderComp = getSheetRenderComponent(unitId, SHEET_VIEW_KEY.COLUMN) as SpreadsheetColumnHeader;
if (!columnRenderComp) return;
const columnHeaderExt = columnRenderComp.extensions.get('DefaultColumnHeaderLayoutExtension')! as ColumnHeaderLayout;

const spy = vi.spyOn(columnHeaderExt, 'draw');

univerAPI.customizeColumnHeader({ headerStyle: { backgroundColor: 'pink', fontSize: 9 }, columnsCfg: ['ASC', 'MokaII', undefined, { text: 'Size', textAlign: 'left' }, { text: 'MUJI', fontSize: 15, textAlign: 'right' }, { text: 'SRI-RESOLVE', fontSize: 10, textAlign: 'left', fontColor: 'blue', backgroundColor: 'wheat' }, null, null, 'ss', { fontSize: 29, fontColor: 'red', text: 'hash' }] });
expect(columnHeaderExt.headerStyle.backgroundColor).toBe('pink');
expect(columnHeaderExt.headerStyle.fontSize).toBe(9);
expect(columnHeaderExt.headerStyle.borderColor).toBe('rgb(217,217,217)');
expect(columnHeaderExt.columnsCfg.length).toBe(10);

vi.advanceTimersByTime(16); // 模拟时间流逝
expect(spy).toHaveBeenCalled();
});
});
19 changes: 18 additions & 1 deletion packages/facade/src/apis/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { IRegisterFunctionService, RegisterFunctionService } from '@univerjs/she
import type { Dependency, IDisposable } from '@wendellhu/redi';
import { Inject, Injector, Quantity } from '@wendellhu/redi';

import type { RenderComponentType, SheetComponent, SheetExtension } from '@univerjs/engine-render';
import type { IColumnsHeaderCfgParam, RenderComponentType, SheetComponent, SheetExtension, SpreadsheetColumnHeader } from '@univerjs/engine-render';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SHEET_VIEW_KEY } from '@univerjs/sheets-ui';
import { SetFormulaCalculationStartMutation } from '@univerjs/engine-formula';
Expand Down Expand Up @@ -346,6 +346,23 @@ export class FUniver {
return renderComponent;
}

/**
* customizeColumnHeader
* @param cfg
* cfg example
({ headerStyle:{backgroundColor: 'pink', fontSize: 9}, columnsCfg: ['ASC', 'MokaII', undefined, null, {text: 'Size', textAlign: 'left'}, {text: 'MUJI', fontSize: 15, textAlign: 'right'}, {text: 'SRI-RESOLVE', fontSize: 10, textAlign: 'left', fontColor: 'blue', backgroundColor: 'wheat'}]})
*/
customizeColumnHeader(cfg: IColumnsHeaderCfgParam) {
const wb = this.getActiveWorkbook();
if (!wb) {
console.error('WorkBook not exist');
return;
}
const unitId = wb?.getId();
const sheetColumn = this._getSheetRenderComponent(unitId, SHEET_VIEW_KEY.COLUMN) as SpreadsheetColumnHeader;
sheetColumn.setCustomHeader(cfg);
}

private _initialize(): void {
this._debouncedFormulaCalculation = debounce(() => {
this._commandService.executeCommand(
Expand Down

0 comments on commit fd2ee17

Please sign in to comment.