mirror of
https://github.com/dream-num/univer.git
synced 2026-09-19 02:18:42 +08:00
feat(sheets-ui): improve large sheet loading preview (#7297)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user