feat(sheets-ui): improve large sheet loading preview (#7297)

This commit is contained in:
Univer
2026-07-20 18:16:26 +08:00
committed by GitHub
parent d37bfc1878
commit 1fb69c0c7c
6 changed files with 507 additions and 43 deletions
@@ -0,0 +1,51 @@
/**
* 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 { IConfigService, IUniverInstanceService, Univer, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { describe, expect, it, vi } from 'vitest';
import { UniverSheetsUIPlugin } from '../plugin';
import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service';
describe('UniverSheetsUIPlugin', () => {
it('registers basic render modules during Starting', () => {
const univer = new Univer();
const injector = univer.__getInjector();
injector.add([ComponentManager]);
injector.add([IconManager]);
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
const renderManagerService = injector.get(IRenderManagerService);
const registerRenderModule = vi.spyOn(renderManagerService, 'registerRenderModule');
const plugin = new UniverSheetsUIPlugin(
undefined,
injector,
renderManagerService,
injector.get(IConfigService),
injector.get(IUniverInstanceService)
);
plugin.onStarting();
expect(registerRenderModule).toHaveBeenCalledWith(
UniverInstanceType.UNIVER_SHEET,
[SheetSkeletonManagerService]
);
univer.dispose();
});
});
+4 -2
View File
@@ -204,6 +204,10 @@ export class UniverSheetsUIPlugin extends Plugin {
[SheetPermissionInterceptorClipboardController],
[SheetPermissionCheckUIController],
] as Dependency[], this._config.override));
// A sheet renderer can be created before Ready when another sheet-type plugin
// eagerly resolves SheetsRenderService during Starting.
this._registerRenderBasics();
}
override onReady(): void {
@@ -215,8 +219,6 @@ export class UniverSheetsUIPlugin extends Plugin {
[SheetClipboardController],
]);
this._registerRenderBasics();
touchDependencies(this._injector, [
[SheetUIController],
[SheetsRenderService],
@@ -15,7 +15,6 @@
*/
import type { IWorkbookData } from '@univerjs/core';
import type { ILayoutService } from '@univerjs/ui';
import {
ContextService,
DesktopLogService,
@@ -25,10 +24,14 @@ import {
InterceptorEffectEnum,
IUniverInstanceService,
LocaleType,
ThemeService,
toDisposable,
UniverInstanceService,
Workbook,
} from '@univerjs/core';
import { Engine, IRenderManagerService, RenderManagerService, RenderUnit, Scene } from '@univerjs/engine-render';
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { ILayoutService } from '@univerjs/ui';
import { describe, expect, it, vi } from 'vitest';
import { SheetLoadingRenderService } from '../sheet-loading-render.service';
@@ -38,12 +41,19 @@ vi.mock('@univerjs/engine-render', async (importOriginal) => {
class MockScene {
disableObjectsEvent = vi.fn();
makeDirty = vi.fn();
render = vi.fn();
requestRender = vi.fn(async () => {});
dispose = vi.fn();
}
class MockEngine {
private readonly _canvasElement = document.createElement('canvas');
constructor() {
this._canvasElement.style.position = 'absolute';
this._canvasElement.style.zIndex = '8';
}
getCanvas() {
return {
setId: (id: string) => { this._canvasElement.id = id; },
@@ -51,6 +61,10 @@ vi.mock('@univerjs/engine-render', async (importOriginal) => {
};
}
getCanvasElement() {
return this._canvasElement;
}
mount(element: HTMLElement) {
element.append(this._canvasElement);
}
@@ -93,28 +107,38 @@ vi.mock('@univerjs/engine-render', async (importOriginal) => {
};
});
function createLayoutService(contentElement: HTMLElement): ILayoutService {
return {
isFocused: false,
rootContainerElement: null,
focus() { },
registerFocusHandler: () => toDisposable(() => {}),
registerRootContainerElement: () => toDisposable(() => {}),
registerContentElement: () => toDisposable(() => {}),
registerContainerElement: () => toDisposable(() => {}),
getContentElement: () => contentElement,
checkElementInCurrentContainers: () => false,
checkContentIsFocused: () => false,
};
}
describe('SheetLoadingRenderService', () => {
it('renders intercepted display values without owning the global workbook or pointer events', () => {
it('renders intercepted display values without owning the global workbook or pointer events', async () => {
const contentElement = document.createElement('div');
contentElement.style.pointerEvents = 'auto';
vi.spyOn(contentElement, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 640, 480));
const animationFrames: FrameRequestCallback[] = [];
const requestAnimationFrame = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
animationFrames.push(callback);
return animationFrames.length;
});
const layoutService: ILayoutService = {
isFocused: false,
rootContainerElement: null,
focus() { },
registerFocusHandler: () => toDisposable(() => {}),
registerRootContainerElement: () => toDisposable(() => {}),
registerContentElement: () => toDisposable(() => {}),
registerContainerElement: () => toDisposable(() => {}),
getContentElement: () => contentElement,
checkElementInCurrentContainers: () => false,
checkContentIsFocused: () => false,
};
const layoutService = createLayoutService(contentElement);
const injector = new Injector();
injector.add([ILogService, { useClass: DesktopLogService }]);
injector.add([IContextService, { useClass: ContextService }]);
injector.add([IUniverInstanceService, { useClass: UniverInstanceService }]);
injector.add([ThemeService]);
injector.add([SheetInterceptorService]);
const interceptorService = injector.get(SheetInterceptorService);
const displayInterceptor = vi.fn((cell, context, next) => next({ ...cell, v: '1,234' }));
@@ -123,7 +147,9 @@ describe('SheetLoadingRenderService', () => {
handler: displayInterceptor,
});
const service = new SheetLoadingRenderService(injector, layoutService, interceptorService);
injector.add([ILayoutService, { useValue: layoutService }]);
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
const service = injector.createInstance(SheetLoadingRenderService);
const workbookData = {
id: 'unit-1',
name: 'Workbook',
@@ -139,12 +165,33 @@ describe('SheetLoadingRenderService', () => {
},
resources: [],
} satisfies IWorkbookData;
const finalWorkbook = injector.createInstance(Workbook, workbookData);
const finalEngine = new Engine('unit-1');
const finalScene = new Scene('unit-1-scene', finalEngine);
const finalRender = injector.createInstance(RenderUnit, {
unit: finalWorkbook,
engine: finalEngine,
scene: finalScene,
isMainScene: true,
});
injector.get(IRenderManagerService).addRender('unit-1', finalRender);
service.showSkeleton(workbookData, 'sheet-1');
expect(service.loading$.value).toBe(true);
expect(service.previewReady$.value).toBe(false);
expect(contentElement.querySelector('canvas')).toBeNull();
service.show(workbookData, 'sheet-1', { 2: { 3: { v: 1234 } } });
finalEngine.mount(contentElement);
animationFrames[0](0);
await vi.waitFor(() => expect(finalEngine.getCanvasElement().style.visibility).toBe('hidden'));
const previewWorkbook = service.workbook$.value;
expect(contentElement.style.pointerEvents).toBe('auto');
expect(contentElement.querySelector('canvas')?.style.pointerEvents).toBe('none');
expect(Array.from(contentElement.querySelectorAll('canvas')).map((canvas) => canvas.style.zIndex)).toEqual(['9', '8']);
expect(service.previewReady$.value).toBe(true);
expect(previewWorkbook?.getSheetBySheetId('sheet-1')?.getCellRaw(2, 3)?.v).toBe('1,234');
expect(displayInterceptor).toHaveBeenCalledWith(
{ v: 1234 },
@@ -159,10 +206,139 @@ describe('SheetLoadingRenderService', () => {
expect.any(Function)
);
service.hide('unit-1');
const handoff = service.handoff('unit-1', 10_000);
await vi.waitFor(() => expect(animationFrames).toHaveLength(2));
expect(finalScene.makeDirty).toHaveBeenCalledWith(true);
expect(finalScene.requestRender).toHaveBeenCalledTimes(1);
expect(service.loading$.value).toBe(true);
expect(contentElement.querySelectorAll('canvas')).toHaveLength(2);
expect(finalEngine.getCanvasElement().style.visibility).toBe('hidden');
animationFrames[1](0);
await expect(handoff).resolves.toBe(true);
expect(service.loading$.value).toBe(false);
expect(contentElement.querySelector('canvas')).toBeNull();
expect(service.previewReady$.value).toBe(false);
expect(contentElement.querySelectorAll('canvas')).toHaveLength(1);
expect(contentElement.querySelector('canvas')?.style.zIndex).toBe('8');
expect(finalEngine.getCanvasElement().style.visibility).toBe('');
service.dispose();
injector.get(IRenderManagerService).dispose();
finalWorkbook.dispose();
finalEngine.dispose();
requestAnimationFrame.mockRestore();
});
it('keeps the preview mounted when the final renderer times out', async () => {
vi.useFakeTimers();
const contentElement = document.createElement('div');
vi.spyOn(contentElement, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 640, 480));
const injector = new Injector();
injector.add([ILogService, { useClass: DesktopLogService }]);
injector.add([IContextService, { useClass: ContextService }]);
injector.add([IUniverInstanceService, { useClass: UniverInstanceService }]);
injector.add([ThemeService]);
injector.add([SheetInterceptorService]);
injector.add([ILayoutService, { useValue: createLayoutService(contentElement) }]);
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
const service = injector.createInstance(SheetLoadingRenderService);
const workbookData = {
id: 'unit-1',
name: 'Workbook',
appVersion: '',
locale: LocaleType.EN_US,
styles: {},
sheetOrder: ['sheet-1'],
sheets: { 'sheet-1': { id: 'sheet-1', cellData: {} } },
resources: [],
} satisfies IWorkbookData;
service.showSkeleton(workbookData, 'sheet-1');
service.show(workbookData, 'sheet-1', {});
const handoff = service.handoff('unit-1', 10_000);
await vi.advanceTimersByTimeAsync(10_000);
await expect(handoff).resolves.toBe(false);
expect(service.loading$.value).toBe(true);
expect(contentElement.querySelector('canvas')).not.toBeNull();
service.dispose();
injector.get(IRenderManagerService).dispose();
vi.useRealTimers();
});
it('keeps the preview mounted until the final canvas is mounted', async () => {
const contentElement = document.createElement('div');
vi.spyOn(contentElement, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 640, 480));
const animationFrames: FrameRequestCallback[] = [];
const requestAnimationFrame = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
animationFrames.push(callback);
return animationFrames.length;
});
const injector = new Injector();
injector.add([ILogService, { useClass: DesktopLogService }]);
injector.add([IContextService, { useClass: ContextService }]);
injector.add([IUniverInstanceService, { useClass: UniverInstanceService }]);
injector.add([ThemeService]);
injector.add([SheetInterceptorService]);
injector.add([ILayoutService, { useValue: createLayoutService(contentElement) }]);
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
const service = injector.createInstance(SheetLoadingRenderService);
const workbookData = {
id: 'unit-1',
name: 'Workbook',
appVersion: '',
locale: LocaleType.EN_US,
styles: {},
sheetOrder: ['sheet-1'],
sheets: { 'sheet-1': { id: 'sheet-1', cellData: {} } },
resources: [],
} satisfies IWorkbookData;
const finalWorkbook = injector.createInstance(Workbook, workbookData);
const finalEngine = new Engine('unit-1');
const finalScene = new Scene('unit-1-scene', finalEngine);
const finalRender = injector.createInstance(RenderUnit, {
unit: finalWorkbook,
engine: finalEngine,
scene: finalScene,
isMainScene: true,
});
injector.get(IRenderManagerService).addRender('unit-1', finalRender);
service.showSkeleton(workbookData, 'sheet-1');
service.show(workbookData, 'sheet-1', {});
const handoff = service.handoff('unit-1', 10_000);
await vi.waitFor(() => expect(animationFrames).toHaveLength(2));
expect(finalScene.requestRender).not.toHaveBeenCalled();
expect(service.loading$.value).toBe(true);
expect(contentElement.querySelectorAll('canvas')).toHaveLength(1);
animationFrames[1](0);
await vi.waitFor(() => expect(animationFrames).toHaveLength(3));
expect(finalScene.requestRender).not.toHaveBeenCalled();
finalEngine.mount(contentElement);
animationFrames[2](0);
await vi.waitFor(() => expect(animationFrames).toHaveLength(4));
expect(finalScene.requestRender).toHaveBeenCalledTimes(1);
expect(service.loading$.value).toBe(true);
expect(contentElement.querySelectorAll('canvas')).toHaveLength(2);
expect(finalEngine.getCanvasElement().style.visibility).toBe('hidden');
animationFrames[3](0);
await expect(handoff).resolves.toBe(true);
expect(service.loading$.value).toBe(false);
expect(contentElement.querySelectorAll('canvas')).toHaveLength(1);
expect(finalEngine.getCanvasElement().style.visibility).toBe('');
service.dispose();
injector.get(IRenderManagerService).dispose();
finalWorkbook.dispose();
finalEngine.dispose();
requestAnimationFrame.mockRestore();
});
});
@@ -15,24 +15,29 @@
*/
import type { ICellData, IObjectMatrixPrimitiveType, IWorkbookData } from '@univerjs/core';
import type { Observable } from 'rxjs';
import type { IRender } from '@univerjs/engine-render';
import type { Subscription } from 'rxjs';
import { createIdentifier, Disposable, Inject, Injector, InterceptorEffectEnum, ObjectMatrix, Workbook } from '@univerjs/core';
import { Engine, RenderUnit, Scene, Spreadsheet } from '@univerjs/engine-render';
import { Engine, IRenderManagerService, RenderUnit, Scene, Spreadsheet } from '@univerjs/engine-render';
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { ILayoutService } from '@univerjs/ui';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, filter, firstValueFrom, from, map, Observable, of, switchMap, take, timeout } from 'rxjs';
import { SheetRenderController } from '../controllers/render-controllers/sheet.render-controller';
import { SheetSkeletonRenderController } from '../controllers/render-controllers/skeleton.render-controller';
import { SheetSkeletonManagerService } from './sheet-skeleton-manager.service';
const PREVIEW_UNIT_SUFFIX = '__snapshot_loading_preview__';
const PREVIEW_SCENE_PREFIX = '_UNIVER_SCENE_';
const PREVIEW_CANVAS_Z_INDEX = '9';
export interface ISheetLoadingRenderService {
readonly workbook$: Observable<Workbook | null>;
readonly loading$: Observable<boolean>;
readonly previewReady$: Observable<boolean>;
showSkeleton(workbookData: IWorkbookData, activeSheetId: string): void;
show(workbookData: IWorkbookData, activeSheetId: string, cellData?: IObjectMatrixPrimitiveType<ICellData>): void;
handoff(sourceUnitId: string, timeoutMs: number): Promise<boolean>;
hide(sourceUnitId: string): void;
}
@@ -41,7 +46,7 @@ export const ISheetLoadingRenderService = createIdentifier<ISheetLoadingRenderSe
interface ILoadingRender {
sourceUnitId: string;
workbook: Workbook;
render: RenderUnit;
render: RenderUnit | null;
lastCellData?: IObjectMatrixPrimitiveType<ICellData>;
}
@@ -52,35 +57,61 @@ interface ILoadingRender {
export class SheetLoadingRenderService extends Disposable implements ISheetLoadingRenderService {
readonly workbook$ = new BehaviorSubject<Workbook | null>(null);
readonly loading$ = new BehaviorSubject(false);
readonly previewReady$ = new BehaviorSubject(false);
private _loadingRender: ILoadingRender | null = null;
private _finalCanvasGuard: Subscription | null = null;
private _hiddenFinalCanvas: HTMLCanvasElement | null = null;
private _finalCanvasVisibility = '';
constructor(
@Inject(Injector) private readonly _injector: Injector,
@ILayoutService private readonly _layoutService: ILayoutService,
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
) {
super();
}
showSkeleton(workbookData: IWorkbookData, activeSheetId: string): void {
const sourceUnitId = workbookData.id;
if (this._loadingRender?.sourceUnitId !== sourceUnitId) {
this._disposeLoadingRender();
this.previewReady$.next(false);
this._loadingRender = {
sourceUnitId,
workbook: this._createLoadingWorkbook(workbookData, activeSheetId),
render: null,
};
this.workbook$.next(this._loadingRender.workbook);
this.loading$.next(true);
this._guardFinalCanvas(sourceUnitId);
}
const worksheet = this._loadingRender.workbook.getSheetBySheetId(activeSheetId);
if (worksheet && this._loadingRender.workbook.getActiveSheet().getSheetId() !== activeSheetId) {
this._loadingRender.workbook.setActiveSheet(worksheet);
}
}
show(
workbookData: IWorkbookData,
activeSheetId: string,
cellData?: IObjectMatrixPrimitiveType<ICellData>
): void {
const sourceUnitId = workbookData.id;
if (this._loadingRender?.sourceUnitId !== sourceUnitId) {
this._disposeLoadingRender();
this._loadingRender = this._createLoadingRender(workbookData, activeSheetId);
this.workbook$.next(this._loadingRender.workbook);
this.loading$.next(true);
}
this.showSkeleton(workbookData, activeSheetId);
const loadingRender = this._loadingRender;
if (!loadingRender) {
return;
}
let shouldRender = false;
if (!loadingRender.render) {
loadingRender.render = this._createLoadingRender(loadingRender.workbook, loadingRender.sourceUnitId);
shouldRender = true;
}
const worksheet = loadingRender.workbook.getSheetBySheetId(activeSheetId);
if (!worksheet) {
return;
@@ -89,6 +120,7 @@ export class SheetLoadingRenderService extends Disposable implements ISheetLoadi
if (loadingRender.workbook.getActiveSheet().getSheetId() !== activeSheetId) {
loadingRender.workbook.setActiveSheet(worksheet);
loadingRender.render.with(SheetSkeletonManagerService).setCurrent({ sheetId: activeSheetId });
shouldRender = true;
}
if (cellData && cellData !== loadingRender.lastCellData) {
@@ -118,6 +150,40 @@ export class SheetLoadingRenderService extends Disposable implements ISheetLoadi
loadingRender.render.mainComponent.makeForceDirty(true);
}
loadingRender.render.scene.makeDirty(true);
shouldRender = true;
}
if (shouldRender) {
loadingRender.render.scene.render();
}
if (!this.previewReady$.value) {
this.previewReady$.next(true);
}
}
async handoff(sourceUnitId: string, timeoutMs: number): Promise<boolean> {
if (this._loadingRender?.sourceUnitId !== sourceUnitId) {
return true;
}
this._finalCanvasGuard?.unsubscribe();
this._finalCanvasGuard = null;
try {
await firstValueFrom(this._getFinalRender$(sourceUnitId).pipe(
take(1),
switchMap((render) => this._waitForFinalCanvas(render).pipe(
switchMap(() => {
this._hideFinalCanvas(render);
return this._requestFinalRender(render);
})
)),
timeout(timeoutMs)
));
this.hide(sourceUnitId);
return true;
} catch {
return false;
}
}
@@ -133,10 +199,11 @@ export class SheetLoadingRenderService extends Disposable implements ISheetLoadi
this._disposeLoadingRender();
this.workbook$.complete();
this.loading$.complete();
this.previewReady$.complete();
super.dispose();
}
private _createLoadingRender(workbookData: IWorkbookData, activeSheetId: string): ILoadingRender {
private _createLoadingWorkbook(workbookData: IWorkbookData, activeSheetId: string): Workbook {
const previewUnitId = `${workbookData.id}${PREVIEW_UNIT_SUFFIX}`;
const workbook = this._injector.createInstance(Workbook, {
...workbookData,
@@ -147,6 +214,11 @@ export class SheetLoadingRenderService extends Disposable implements ISheetLoadi
workbook.setActiveSheet(activeSheet);
}
return workbook;
}
private _createLoadingRender(workbook: Workbook, sourceUnitId: string): RenderUnit {
const previewUnitId = workbook.getUnitId();
const contentElement = this._layoutService.getContentElement();
const { width, height } = contentElement.getBoundingClientRect();
const engine = this._injector.createInstance(Engine, previewUnitId, undefined);
@@ -170,28 +242,124 @@ export class SheetLoadingRenderService extends Disposable implements ISheetLoadi
]);
const canvas = engine.getCanvas();
canvas.setId(`univer-sheet-loading-canvas_${workbookData.id}`);
canvas.getCanvasEle().dataset.uUnitId = workbookData.id;
canvas.setId(`univer-sheet-loading-canvas_${sourceUnitId}`);
canvas.getCanvasEle().dataset.uUnitId = sourceUnitId;
canvas.getCanvasEle().style.pointerEvents = 'none';
canvas.getCanvasEle().style.zIndex = PREVIEW_CANVAS_Z_INDEX;
engine.mount(contentElement);
return { sourceUnitId: workbookData.id, workbook, render };
return render;
} catch (error) {
this._disposeRender(render);
workbook.dispose();
throw error;
}
}
private _waitForFinalCanvas(render: IRender): Observable<void> {
return new Observable((subscriber) => {
let frameId: number | undefined;
const contentElement = this._layoutService.getContentElement();
const canvasElement = render.engine.getCanvasElement();
const checkMounted = () => {
if (
canvasElement.parentElement === contentElement &&
canvasElement.width > 0 &&
canvasElement.height > 0
) {
subscriber.next();
subscriber.complete();
return;
}
frameId = window.requestAnimationFrame(checkMounted);
};
checkMounted();
return () => {
if (frameId !== undefined) {
window.cancelAnimationFrame(frameId);
}
};
});
}
private _getFinalRender$(sourceUnitId: string): Observable<IRender> {
const existingRender = this._renderManagerService.getRenderById(sourceUnitId);
return existingRender
? of(existingRender)
: this._renderManagerService.created$.pipe(
filter((render) => render.unitId === sourceUnitId)
);
}
private _guardFinalCanvas(sourceUnitId: string): void {
this._finalCanvasGuard?.unsubscribe();
this._finalCanvasGuard = this._getFinalRender$(sourceUnitId).pipe(
take(1),
switchMap((render) => this._waitForFinalCanvas(render).pipe(map(() => render)))
).subscribe((render) => this._hideFinalCanvas(render));
}
private _requestFinalRender(render: IRender): Observable<unknown> {
render.scene.makeDirty(true);
return from(render.scene.requestRender()).pipe(
// Scene.requestRender resolves from a requestAnimationFrame callback, before that
// frame is painted. Keep the preview above the final canvas for one more frame so
// the browser paints the final canvas before the preview is disposed.
switchMap(() => this._waitForNextAnimationFrame())
);
}
private _hideFinalCanvas(render: IRender): void {
const canvasElement = render.engine.getCanvasElement();
if (this._hiddenFinalCanvas === canvasElement) {
return;
}
this._restoreFinalCanvas();
this._hiddenFinalCanvas = canvasElement;
this._finalCanvasVisibility = canvasElement.style.visibility;
canvasElement.style.visibility = 'hidden';
}
private _restoreFinalCanvas(): void {
if (!this._hiddenFinalCanvas) {
return;
}
this._hiddenFinalCanvas.style.visibility = this._finalCanvasVisibility;
this._hiddenFinalCanvas = null;
this._finalCanvasVisibility = '';
}
private _waitForNextAnimationFrame(): Observable<void> {
return new Observable((subscriber) => {
const frameId = window.requestAnimationFrame(() => {
subscriber.next();
subscriber.complete();
});
return () => window.cancelAnimationFrame(frameId);
});
}
private _disposeLoadingRender(): void {
this._finalCanvasGuard?.unsubscribe();
this._finalCanvasGuard = null;
this._restoreFinalCanvas();
const loadingRender = this._loadingRender;
this._loadingRender = null;
this.workbook$.next(null);
this.loading$.next(false);
this.previewReady$.next(false);
if (!loadingRender) {
return;
}
this._disposeRender(loadingRender.render);
if (loadingRender.render) {
this._disposeRender(loadingRender.render);
}
loadingRender.workbook.dispose();
}
+6 -1
View File
@@ -29,7 +29,7 @@ export function useActiveWorkbook(): Workbook | null {
const univerInstanceService = useDependency(IUniverInstanceService);
const loadingWorkbook = useContext(SheetLoadingWorkbookContext);
const workbook = useObservable(() => univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET), undefined, undefined, []);
return workbook ?? loadingWorkbook ?? null;
return loadingWorkbook ?? workbook ?? null;
}
export function useSheetLoadingWorkbook(): Workbook | null {
@@ -42,6 +42,11 @@ export function useSheetLoading(): boolean {
return useObservable(loadingRenderService.loading$) ?? false;
}
export function useSheetLoadingPreviewReady(): boolean {
const loadingRenderService = useDependency(ISheetLoadingRenderService);
return useObservable(loadingRenderService.previewReady$) ?? false;
}
export function useActiveWorksheet(workbook?: Workbook | null) {
const worksheet = useObservable(() => workbook?.activeSheet$ ?? of(null), undefined, undefined, [workbook]);
return worksheet;
@@ -16,8 +16,18 @@
import type { Workbook, Worksheet } from '@univerjs/core';
import type { IUniverSheetsUIConfig } from '../../config/config';
import { Injector, isInternalEditorID, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import {
DEFAULT_WORKSHEET_COLUMN_TITLE_HEIGHT,
DEFAULT_WORKSHEET_COLUMN_WIDTH,
DEFAULT_WORKSHEET_ROW_HEIGHT,
DEFAULT_WORKSHEET_ROW_TITLE_WIDTH,
Injector,
isInternalEditorID,
IUniverInstanceService,
UniverInstanceType,
} from '@univerjs/core';
import { clsx } from '@univerjs/design';
import { LoadingMultiIcon } from '@univerjs/icons';
import { ComponentManager, ContextMenuPosition, IMenuManagerService, ToolbarItem, useConfigValue, useDependency, useObservable } from '@univerjs/ui';
import { useEffect, useMemo } from 'react';
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../../config/config';
@@ -31,6 +41,7 @@ import {
useActiveWorkbook,
useActiveWorksheet,
useSheetLoading,
useSheetLoadingPreviewReady,
useSheetLoadingWorkbook,
} from '../hook';
import { SheetBar } from '../sheet-bar/SheetBar';
@@ -45,7 +56,7 @@ export function RenderSheetFooter() {
const showFooter = config?.footer ?? true;
const activeWorkbook = useActiveWorkbook();
const loadingWorkbook = useSheetLoadingWorkbook();
const workbook = activeWorkbook ?? loadingWorkbook;
const workbook = loadingWorkbook ?? activeWorkbook;
const isLoading = useSheetLoading();
const activeWorkbookEmbeddedRender = useActiveWorkbookIsEmbeddedRender(workbook);
const focusedUnitType = useFocusedUnitType();
@@ -104,7 +115,7 @@ export function RenderSheetHeader() {
const config = useConfigValue<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY);
const activeWorkbook = useActiveWorkbook();
const loadingWorkbook = useSheetLoadingWorkbook();
const workbook = activeWorkbook ?? loadingWorkbook;
const workbook = loadingWorkbook ?? activeWorkbook;
const isLoading = useSheetLoading();
const hasWorkbook = !!workbook;
const activeWorkbookEmbeddedRender = useActiveWorkbookIsEmbeddedRender(workbook);
@@ -144,8 +155,11 @@ export function RenderSheetHeader() {
export function RenderSheetContent() {
const config = useConfigValue<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY);
const isLoading = useSheetLoading();
const isPreviewReady = useSheetLoadingPreviewReady();
const componentManager = useDependency(ComponentManager);
const workbook = useActiveWorkbook();
const activeWorkbook = useActiveWorkbook();
const loadingWorkbook = useSheetLoadingWorkbook();
const workbook = loadingWorkbook ?? activeWorkbook;
const activeEmbedTab = useActiveSheetEmbedTabData(workbook);
const injector = useDependency(Injector);
const activeWorkbookEmbeddedRender = useActiveWorkbookIsEmbeddedRender(workbook);
@@ -164,7 +178,8 @@ export function RenderSheetContent() {
tryGetSheetEmbedRuntimeService(injector)?.clearTab();
}, [activeEmbedTab, activeWorkbookEmbeddedRender, injector, isLoading, workbook]);
if (!workbook || isLoading) return null;
if (!workbook) return null;
if (isLoading) return isPreviewReady ? null : <SheetLoadingSkeleton />;
if (activeWorkbookEmbeddedRender) return null;
if (activeEmbedTab && workbook) {
return <RenderSheetEmbedTabHost workbook={workbook} worksheet={activeEmbedTab.worksheet} />;
@@ -179,6 +194,53 @@ export function RenderSheetContent() {
);
}
function SheetLoadingSkeleton() {
return (
<div
data-u-comp="sheet-loading-skeleton"
aria-busy
className="
univer-absolute univer-inset-0 univer-z-10 univer-overflow-hidden univer-bg-white
dark:!univer-bg-gray-900
"
>
<div
className="
univer-absolute univer-right-0 univer-top-0 univer-border-b univer-border-gray-200 univer-bg-gray-50
dark:!univer-border-gray-700 dark:!univer-bg-gray-800
"
style={{ left: DEFAULT_WORKSHEET_ROW_TITLE_WIDTH, height: DEFAULT_WORKSHEET_COLUMN_TITLE_HEIGHT }}
/>
<div
className="
univer-absolute univer-bottom-0 univer-left-0 univer-border-r univer-border-gray-200 univer-bg-gray-50
dark:!univer-border-gray-700 dark:!univer-bg-gray-800
"
style={{ top: DEFAULT_WORKSHEET_COLUMN_TITLE_HEIGHT, width: DEFAULT_WORKSHEET_ROW_TITLE_WIDTH }}
/>
<div
className="
univer-absolute univer-bottom-0 univer-right-0 univer-animate-pulse univer-text-gray-200
dark:!univer-text-gray-700
"
style={{
top: DEFAULT_WORKSHEET_COLUMN_TITLE_HEIGHT,
left: DEFAULT_WORKSHEET_ROW_TITLE_WIDTH,
backgroundImage: 'linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)',
backgroundSize: `${DEFAULT_WORKSHEET_COLUMN_WIDTH}px ${DEFAULT_WORKSHEET_ROW_HEIGHT}px`,
}}
/>
<LoadingMultiIcon
aria-hidden
className="
univer-absolute univer-left-1/2 univer-top-1/2 univer-size-6 -univer-translate-x-1/2
-univer-translate-y-1/2 univer-animate-spin univer-text-violet-500
"
/>
</div>
);
}
function RenderSheetEmbedTabHost(props: { workbook: Workbook; worksheet: Worksheet }) {
const { workbook, worksheet } = props;
const injector = useDependency(Injector);