mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 14:56:51 +08:00
fix(docs): stabilize print preparation and column layout (#7552)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user