revert(ui): restore unit-scoped runtime routing (#7472)

This commit is contained in:
Univer
2026-08-11 14:22:45 +08:00
committed by GitHub
parent 4636f66a05
commit 825bf011f2
5 changed files with 16 additions and 165 deletions
@@ -28,7 +28,6 @@ export const FOCUSING_SHEET = 'FOCUSING_SHEET';
export const FOCUSING_DOC = 'FOCUSING_DOC';
export const FOCUSING_SLIDE = 'FOCUSING_SLIDE';
export const FOCUSING_BOARD = 'FOCUSING_BOARD';
export const FOCUSING_BASE = 'FOCUSING_BASE';
export const EDITOR_ACTIVATED = 'EDITOR_ACTIVATED';
export const FOCUSING_EDITOR_INPUT_FORMULA = 'FOCUSING_EDITOR_INPUT_FORMULA';
@@ -1,93 +0,0 @@
/**
* 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.
*/
/**
* @vitest-environment jsdom
*/
import { describe, expect, it } from 'vitest';
import { UIRuntimeScopeService } from '../ui-runtime-scope.service';
describe('UIRuntimeScopeService', () => {
it('resolves concurrent runtimes for the same unit from their DOM roots', () => {
const service = new UIRuntimeScopeService();
const floatingRoot = document.createElement('div');
const fullscreenRoot = document.createElement('div');
const floatingTarget = document.createElement('textarea');
const fullscreenTarget = document.createElement('textarea');
floatingRoot.appendChild(floatingTarget);
fullscreenRoot.appendChild(fullscreenTarget);
const floatingScope = {
unitId: 'base-1',
root: floatingRoot,
has: () => false,
get<T>(): T {
throw new Error('not implemented');
},
};
const fullscreenScope = {
unitId: 'base-1',
root: fullscreenRoot,
has: () => false,
get<T>(): T {
throw new Error('not implemented');
},
};
const floatingDisposable = service.register(floatingScope);
const fullscreenDisposable = service.register(fullscreenScope);
expect(service.get('base-1')).toBe(fullscreenScope);
expect(service.get('base-1', floatingTarget)).toBe(floatingScope);
expect(service.get('base-1', fullscreenTarget)).toBe(fullscreenScope);
fullscreenDisposable.dispose();
expect(service.get('base-1')).toBe(floatingScope);
floatingDisposable.dispose();
expect(service.get('base-1')).toBeUndefined();
});
it('keeps the unit id as the runtime boundary when resolving an event target', () => {
const service = new UIRuntimeScopeService();
const baseRoot = document.createElement('div');
const sheetRoot = document.createElement('div');
const sheetTarget = document.createElement('textarea');
sheetRoot.appendChild(sheetTarget);
const baseScope = {
unitId: 'base-1',
root: baseRoot,
has: () => false,
get<T>(): T {
throw new Error('not implemented');
},
};
const sheetScope = {
unitId: 'sheet-1',
root: sheetRoot,
has: () => false,
get<T>(): T {
throw new Error('not implemented');
},
};
service.register(baseScope);
service.register(sheetScope);
expect(service.get('base-1', sheetTarget)).toBe(baseScope);
expect(service.get('sheet-1', sheetTarget)).toBe(sheetScope);
expect(service.get(undefined, sheetTarget)).toBeUndefined();
});
});
@@ -14,54 +14,36 @@
* limitations under the License.
*/
import type { DependencyIdentifier, IDisposable } from '@univerjs/core';
import type { IDisposable } from '@univerjs/core';
import { createIdentifier, Disposable, toDisposable } from '@univerjs/core';
export interface IUIRuntimeScope {
unitId: string;
root?: HTMLElement;
has<T>(identifier: DependencyIdentifier<T>): boolean;
get<T>(identifier: DependencyIdentifier<T>): T;
has(identifier: unknown): boolean;
get<T = unknown>(identifier: unknown): T;
}
export interface IUIRuntimeScopeService {
register(scope: IUIRuntimeScope): IDisposable;
get(unitId: string | null | undefined, target?: EventTarget | null): IUIRuntimeScope | undefined;
get(unitId: string | null | undefined): IUIRuntimeScope | undefined;
}
export const IUIRuntimeScopeService = createIdentifier<IUIRuntimeScopeService>('ui.runtime-scope.service');
export class UIRuntimeScopeService extends Disposable implements IUIRuntimeScopeService {
private readonly _scopes = new Map<string, IUIRuntimeScope[]>();
private readonly _scopes = new Map<string, IUIRuntimeScope>();
register(scope: IUIRuntimeScope): IDisposable {
const scopes = this._scopes.get(scope.unitId) ?? [];
scopes.push(scope);
this._scopes.set(scope.unitId, scopes);
this._scopes.set(scope.unitId, scope);
return toDisposable(() => {
const registeredScopes = this._scopes.get(scope.unitId);
const index = registeredScopes?.lastIndexOf(scope) ?? -1;
if (index >= 0) {
registeredScopes?.splice(index, 1);
}
if (!registeredScopes?.length) {
if (this._scopes.get(scope.unitId) === scope) {
this._scopes.delete(scope.unitId);
}
});
}
get(unitId: string | null | undefined, target?: EventTarget | null): IUIRuntimeScope | undefined {
const scopes = unitId ? this._scopes.get(unitId) : undefined;
if (scopes && typeof Node !== 'undefined' && target instanceof Node) {
for (let index = scopes.length - 1; index >= 0; index--) {
const scope = scopes[index];
if (scope.root?.contains(target)) {
return scope;
}
}
}
return scopes?.[scopes.length - 1];
get(unitId: string | null | undefined): IUIRuntimeScope | undefined {
return unitId ? this._scopes.get(unitId) : undefined;
}
}
@@ -18,7 +18,7 @@
* @vitest-environment jsdom
*/
import { FOCUSING_BASE, ICommandService, IContextService, Injector } from '@univerjs/core';
import { ICommandService, IContextService, Injector } from '@univerjs/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { EMBED_CHILD_UNIT_ID_ATTRIBUTE } from '../../../utils/embed-boundary';
import { ILayoutService } from '../../layout/layout.service';
@@ -304,7 +304,7 @@ describe('ShortcutService', () => {
});
window.dispatchEvent(event);
expect(getRuntimeScope).toHaveBeenCalledWith('child-board', canvas);
expect(getRuntimeScope).toHaveBeenCalledWith('child-board');
expect(scopedContextService.getContextValue).toHaveBeenCalledWith('FOCUSING_BOARD');
expect(scopedExecuteCommand).toHaveBeenCalledWith('board.operation.grouping-shortcut', undefined);
expect(executeCommand).not.toHaveBeenCalled();
@@ -312,44 +312,6 @@ describe('ShortcutService', () => {
service.dispose();
});
it('uses the runtime that owns the event target when the same child unit has multiple runtimes', () => {
const runtime = document.createElement('div');
const canvas = document.createElement('canvas');
runtime.setAttribute(EMBED_CHILD_UNIT_ID_ATTRIBUTE, 'child-base');
runtime.appendChild(canvas);
const scopedExecuteCommand = vi.fn(() => Promise.resolve(true));
const scopedContextService = {
getContextValue: vi.fn((key: string) => key === FOCUSING_BASE),
};
const targetRuntimeScope = {
has: vi.fn(() => true),
get: vi.fn((identifier: unknown) => identifier === IContextService
? scopedContextService
: { executeCommand: scopedExecuteCommand }),
};
const { service, executeCommand, getRuntimeScope } = createService();
getRuntimeScope.mockReturnValue(targetRuntimeScope);
service.registerShortcut({
id: 'base.operation.edit-cell',
binding: KeyCode.ENTER,
preconditions: (contextService) => contextService.getContextValue(FOCUSING_BASE),
});
const event = createKeyboardEvent(KeyCode.ENTER);
Object.defineProperty(event, 'target', {
configurable: true,
get: () => canvas,
});
window.dispatchEvent(event);
expect(getRuntimeScope).toHaveBeenCalledWith('child-base', canvas);
expect(scopedExecuteCommand).toHaveBeenCalledWith('base.operation.edit-cell', undefined);
expect(executeCommand).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
service.dispose();
});
it('lets embed-owned native text editors handle select-all without dispatching Univer shortcuts', () => {
const embedRoot = document.createElement('div');
embedRoot.setAttribute('data-embed-interaction-boundary-owner', 'embed-1');
@@ -422,7 +384,7 @@ describe('ShortcutService', () => {
});
window.dispatchEvent(event);
expect(getRuntimeScope).toHaveBeenCalledWith('child-doc', textEditor);
expect(getRuntimeScope).toHaveBeenCalledWith('child-doc');
expect(scopedExecuteCommand).toHaveBeenCalledWith('doc.command.select-all', undefined);
expect(executeCommand).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { DependencyIdentifier, IDisposable } from '@univerjs/core';
import type { IDisposable } from '@univerjs/core';
import type { Observable } from 'rxjs';
import { createIdentifier, Disposable, ICommandService, IContextService, Optional, toDisposable } from '@univerjs/core';
import { Subject } from 'rxjs';
@@ -397,8 +397,9 @@ export class ShortcutService extends Disposable implements IShortcutService {
return isNativeTextEditor && isEmbedBoundaryTarget(target);
}
private _getRuntimeService<T>(event: KeyboardEvent, identifier: DependencyIdentifier<T>): T | undefined {
const runtimeScope = this._runtimeScopeService.get(getEmbedChildUnitId(event.target), event.target);
private _getRuntimeService<T>(event: KeyboardEvent, identifier: unknown): T | undefined {
const childUnitId = getEmbedChildUnitId(event.target);
const runtimeScope = this._runtimeScopeService.get(childUnitId);
return runtimeScope?.has(identifier) ? runtimeScope.get<T>(identifier) : undefined;
}
}