From 9954e5274343a0863fc7be39b9137f316cfef734 Mon Sep 17 00:00:00 2001 From: Univer <68851825+DR-Univer@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:54:34 +0800 Subject: [PATCH] fix(docs): stabilize print preparation and column layout (#7552) --- packages/docs-ui/src/index.ts | 1 + .../doc-print-interceptor.service.spec.ts | 15 ++++++- .../services/doc-print-interceptor.service.ts | 20 ++++++++++ .../layout/block/__tests__/column.spec.ts | 39 +++++++++++++++++++ .../components/docs/layout/block/column.ts | 29 ++++++-------- 5 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 packages/engine-render/src/components/docs/layout/block/__tests__/column.spec.ts diff --git a/packages/docs-ui/src/index.ts b/packages/docs-ui/src/index.ts index d7b76b6626..7dcd0c8fd3 100644 --- a/packages/docs-ui/src/index.ts +++ b/packages/docs-ui/src/index.ts @@ -247,6 +247,7 @@ export type { ISetDocInputStyleCommandParams } from './services/doc-menu-style.s export { DocPageLayoutService } from './services/doc-page-layout.service'; export { DocParagraphMenuService } from './services/doc-paragraph-menu.service'; export { calcDocRangePositions, DocCanvasPopManagerService } from './services/doc-popup-manager.service'; +export type { IDocPrintPreparationContext } from './services/doc-print-interceptor.service'; export { DocPrintInterceptorService } from './services/doc-print-interceptor.service'; export type { IDocPrintComponentContext, IDocPrintContext } from './services/doc-print-interceptor.service'; export { DocsRenderService } from './services/docs-render.service'; diff --git a/packages/docs-ui/src/services/__tests__/doc-print-interceptor.service.spec.ts b/packages/docs-ui/src/services/__tests__/doc-print-interceptor.service.spec.ts index 5bea1793d8..1b07068de4 100644 --- a/packages/docs-ui/src/services/__tests__/doc-print-interceptor.service.spec.ts +++ b/packages/docs-ui/src/services/__tests__/doc-print-interceptor.service.spec.ts @@ -15,7 +15,7 @@ */ import { Injector } from '@univerjs/core'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { DocPrintInterceptorService } from '../doc-print-interceptor.service'; function createService(): DocPrintInterceptorService { @@ -36,4 +36,17 @@ describe('DocPrintInterceptorService', () => { expect(service.interceptor.fetchThroughInterceptors(interceptPoints.PRINTING_COMPONENT_COLLECT)(undefined, { unitId: 'doc-1' } as never)).toBeUndefined(); expect(service.interceptor.fetchThroughInterceptors(interceptPoints.PRINTING_DOM_COLLECT)(domCollection as never, { unitId: 'doc-1' } as never)).toBe(domCollection); }); + + it('waits for registered print preparation handlers', async () => { + const service = createService(); + const handler = vi.fn(async () => undefined); + const dispose = service.registerPrintPreparation(handler); + + await service.preparePrint({ unitId: 'doc-1', dpr: 2 }); + + expect(handler).toHaveBeenCalledWith({ unitId: 'doc-1', dpr: 2 }); + dispose(); + await service.preparePrint({ unitId: 'doc-2', dpr: 1 }); + expect(handler).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/docs-ui/src/services/doc-print-interceptor.service.ts b/packages/docs-ui/src/services/doc-print-interceptor.service.ts index 24dee0b995..96575802ec 100644 --- a/packages/docs-ui/src/services/doc-print-interceptor.service.ts +++ b/packages/docs-ui/src/services/doc-print-interceptor.service.ts @@ -34,11 +34,17 @@ export interface IDocPrintComponentContext extends IDocPrintContext { documents: Documents; } +export interface IDocPrintPreparationContext { + unitId: string; + dpr: number; +} + const PRINTING_COMPONENT_COLLECT = createInterceptorKey('PRINTING_COMPONENT_COLLECT'); const PRINTING_DOM_COLLECT = createInterceptorKey('PRINTING_DOM_COLLECT'); export class DocPrintInterceptorService extends Disposable { private _printComponentMap: Map = new Map(); + private readonly _printPreparationHandlers = new Set<(context: IDocPrintPreparationContext) => Promise>(); readonly interceptor = new InterceptorManager({ PRINTING_COMPONENT_COLLECT, @@ -66,4 +72,18 @@ export class DocPrintInterceptorService extends Disposable { getPrintComponent(componentKey: string) { return this._printComponentMap.get(componentKey); } + + registerPrintPreparation(handler: (context: IDocPrintPreparationContext) => Promise) { + this._printPreparationHandlers.add(handler); + return () => this._printPreparationHandlers.delete(handler); + } + + async preparePrint(context: IDocPrintPreparationContext): Promise { + await Promise.all([...this._printPreparationHandlers].map((handler) => handler(context))); + } + + override dispose(): void { + this._printPreparationHandlers.clear(); + super.dispose(); + } } diff --git a/packages/engine-render/src/components/docs/layout/block/__tests__/column.spec.ts b/packages/engine-render/src/components/docs/layout/block/__tests__/column.spec.ts new file mode 100644 index 0000000000..b91b5c9581 --- /dev/null +++ b/packages/engine-render/src/components/docs/layout/block/__tests__/column.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023-present DreamNum Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ColumnLayoutType, ColumnResponsiveType } from '@univerjs/core'; +import { describe, expect, it } from 'vitest'; +import { calculateColumnGroupLayout } from '../column'; + +describe('column group layout', () => { + it('compresses multiple columns without looping on floating-point residue', () => { + const layout = calculateColumnGroupLayout({ + columnGroupId: 'column-group-1', + columns: [ + { columnId: 'column-1', widthRatio: 1, minWidth: { v: 50 } }, + { columnId: 'column-2', widthRatio: 1 }, + { columnId: 'column-3', widthRatio: 1 }, + ], + gap: { v: 0 }, + layout: ColumnLayoutType.FIXED, + responsive: ColumnResponsiveType.SHRINK, + }, 100, [0, 0, 0]); + + expect(layout.columns[0].width).toBe(50); + expect(layout.columns[1].width).toBeCloseTo(25); + expect(layout.columns[2].width).toBeCloseTo(25); + }); +}); diff --git a/packages/engine-render/src/components/docs/layout/block/column.ts b/packages/engine-render/src/components/docs/layout/block/column.ts index ceca806202..cebb818729 100644 --- a/packages/engine-render/src/components/docs/layout/block/column.ts +++ b/packages/engine-render/src/components/docs/layout/block/column.ts @@ -205,7 +205,7 @@ function getNextBlockTop(lines: IDocumentSkeletonLine[]) { return lastLine.top + lastLine.lineHeight; } -function calculateColumnGroupLayout(source: IColumnGroup, availableWidth: number, columnHeights: number[]): IColumnGroupLayout { +export function calculateColumnGroupLayout(source: IColumnGroup, availableWidth: number, columnHeights: number[]): IColumnGroupLayout { const width = Math.max(0, availableWidth); const gap = Math.max(0, source.gap?.v ?? 0); const columns = source.columns; @@ -298,28 +298,21 @@ function allocateHorizontalWidths(columns: IColumn[], contentWidth: number): num function compressToFit(widths: number[], minWidths: number[], overflow: number): number[] { const nextWidths = [...widths]; - let remainingOverflow = overflow; - let flexibleIndexes = getFlexibleIndexes(nextWidths, minWidths); - - while (remainingOverflow > 0 && flexibleIndexes.length > 0) { - const totalShrink = flexibleIndexes.reduce((sum, item) => sum + item.shrink, 0); - for (const item of flexibleIndexes) { - const shrink = Math.min(item.shrink, remainingOverflow * item.shrink / totalShrink); - nextWidths[item.index] -= shrink; - remainingOverflow -= shrink; - } - flexibleIndexes = getFlexibleIndexes(nextWidths, minWidths); + const flexibleIndexes = nextWidths + .map((width, index) => ({ index, shrink: Math.max(0, width - minWidths[index]) })) + .filter((item) => item.shrink > 0); + const totalShrink = flexibleIndexes.reduce((sum, item) => sum + item.shrink, 0); + const appliedOverflow = Math.min(overflow, totalShrink); + if (appliedOverflow <= 0 || totalShrink <= 0) { + return nextWidths; + } + for (const item of flexibleIndexes) { + nextWidths[item.index] -= appliedOverflow * item.shrink / totalShrink; } return nextWidths; } -function getFlexibleIndexes(widths: number[], minWidths: number[]) { - return widths - .map((width, index) => ({ index, shrink: Math.max(0, width - minWidths[index]) })) - .filter((item) => item.shrink > 0); -} - function getMinWidth(column: IColumn): number { return Math.max(0, column.minWidth?.v ?? 0); }