fix(docs): stabilize print preparation and column layout (#7552)

This commit is contained in:
Univer
2026-08-18 21:54:34 +08:00
committed by GitHub
parent 8c856a5bc0
commit 9954e52743
5 changed files with 85 additions and 19 deletions
+1
View File
@@ -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';
@@ -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();
});
});
@@ -34,11 +34,17 @@ export interface IDocPrintComponentContext extends IDocPrintContext {
documents: Documents;
}
export interface IDocPrintPreparationContext {
unitId: string;
dpr: number;
}
const PRINTING_COMPONENT_COLLECT = createInterceptorKey<undefined, IDocPrintComponentContext>('PRINTING_COMPONENT_COLLECT');
const PRINTING_DOM_COLLECT = createInterceptorKey<DisposableCollection, IDocPrintDomtContext>('PRINTING_DOM_COLLECT');
export class DocPrintInterceptorService extends Disposable {
private _printComponentMap: Map<string, string> = new Map();
private readonly _printPreparationHandlers = new Set<(context: IDocPrintPreparationContext) => Promise<void>>();
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<void>) {
this._printPreparationHandlers.add(handler);
return () => this._printPreparationHandlers.delete(handler);
}
async preparePrint(context: IDocPrintPreparationContext): Promise<void> {
await Promise.all([...this._printPreparationHandlers].map((handler) => handler(context)));
}
override dispose(): void {
this._printPreparationHandlers.clear();
super.dispose();
}
}
@@ -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);
});
});
@@ -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);
}