snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
# @univerjs/sheets-crosshair-highlight
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-crosshair-highlight?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-crosshair-highlight)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-crosshair-highlight?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-crosshair-highlight)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets-crosshair-highlight?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-crosshair-highlight)
`@univerjs/sheets-crosshair-highlight` adds crosshair highlighting for the active row and column in Univer Sheets.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/sheets-crosshair-highlight` | `UniverSheetsCrosshairHighlight` | Yes | Yes | Yes |
## Installation
```sh
pnpm add @univerjs/sheets-crosshair-highlight
# or
npm install @univerjs/sheets-crosshair-highlight
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import '@univerjs/sheets-crosshair-highlight/lib/index.css';
import EnUS from '@univerjs/sheets-crosshair-highlight/locale/en-US';
import { UniverSheetsCrosshairHighlightPlugin } from '@univerjs/sheets-crosshair-highlight';
univer.registerPlugin(UniverSheetsCrosshairHighlightPlugin);
// Merge EnUS into your Univer locale map when this package contributes UI text.
```
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/sheets-crosshair-highlight)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,105 @@
{
"name": "@univerjs/sheets-crosshair-highlight",
"version": "1.0.0-rc.0",
"private": false,
"description": "Crosshair highlight plugin for Univer Sheets.",
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
"license": "Apache-2.0",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/univer"
},
"homepage": "https://univer.ai",
"repository": {
"type": "git",
"url": "https://github.com/dream-num/univer"
},
"bugs": {
"url": "https://github.com/dream-num/univer/issues"
},
"keywords": [
"univer",
"sheets",
"crosshair",
"highlight",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./locale/*": "./src/locale/*.ts",
"./facade": "./src/facade/index.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./locale/*": {
"import": "./lib/es/locale/*.js",
"require": "./lib/cjs/locale/*.js",
"types": "./lib/types/locale/*.d.ts"
},
"./facade": {
"import": "./lib/es/facade.js",
"require": "./lib/cjs/facade.js",
"types": "./lib/types/facade/index.d.ts"
},
"./lib/facade": {
"import": "./lib/es/facade.js",
"require": "./lib/cjs/facade.js",
"types": "./lib/types/facade/index.d.ts"
},
"./lib/*": "./lib/*"
}
},
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"build:bundle": "univer-cli build",
"build:types": "tsc -p tsconfig.node.json",
"build": "pnpm run build:bundle && pnpm run build:types"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "1.38.0",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"postcss": "^8.5.28",
"react": "18.3.1",
"rxjs": "^7.8.2",
"tailwindcss": "3.4.18",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@univerjs-infra/shared/postcss';

View File

@@ -0,0 +1,88 @@
/**
* 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 { RANGE_TYPE, Rectangle } from '@univerjs/core';
import { describe, expect, it, vi } from 'vitest';
import { CrossHairRangeCollection } from '../util';
describe('CrossHairRangeCollection', () => {
it('should add/split/reset ranges and skip full row/column/all ranges', () => {
const collection = new CrossHairRangeCollection();
collection.addRange({
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
rangeType: RANGE_TYPE.COLUMN,
} as never);
collection.addRange({
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
rangeType: RANGE_TYPE.ROW,
} as never);
collection.addRange({
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
rangeType: RANGE_TYPE.ALL,
} as never);
expect(collection.getRanges()).toEqual([]);
collection.setSelectedRanges([{
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
} as never]);
collection.addRange({
startRow: 0,
endRow: 2,
startColumn: 0,
endColumn: 2,
} as never);
expect(collection.getRanges().length).toBeGreaterThan(0);
const subtractSpy = vi.spyOn(Rectangle, 'subtract').mockReturnValueOnce([{
startRow: 2,
endRow: 1,
startColumn: 0,
endColumn: 0,
} as never]);
collection.addRange({
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
} as never);
expect(subtractSpy).toHaveBeenCalled();
vi.spyOn(Rectangle, 'subtract').mockReturnValueOnce(undefined as never);
collection.addRange({
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
} as never);
collection.reset();
expect(collection.getRanges()).toEqual([]);
});
});

View File

@@ -0,0 +1,118 @@
/**
* 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 {
CommandService,
ConfigService,
ContextService,
DesktopLogService,
ICommandService,
IConfigService,
IContextService,
ILogService,
Injector,
ThemeService,
} from '@univerjs/core';
import { firstValueFrom } from 'rxjs';
import { beforeEach, describe, expect, it } from 'vitest';
import {
CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS,
SheetsCrosshairHighlightService,
} from '../../../services/crosshair.service';
import {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
ToggleCrosshairHighlightOperation,
} from '../operation';
function createOperationTestBed() {
const injector = new Injector();
injector.add([ICommandService, { useClass: CommandService }]);
injector.add([ILogService, { useClass: DesktopLogService }]);
injector.add([IContextService, { useClass: ContextService }]);
injector.add([IConfigService, { useClass: ConfigService }]);
injector.add([ThemeService]);
injector.add([SheetsCrosshairHighlightService]);
const commandService = injector.get(ICommandService);
commandService.registerCommand(ToggleCrosshairHighlightOperation);
commandService.registerCommand(EnableCrosshairHighlightOperation);
commandService.registerCommand(DisableCrosshairHighlightOperation);
commandService.registerCommand(SetCrosshairHighlightColorOperation);
const themeService = injector.get(ThemeService);
const highlightBackground: Array<{ color: string; alpha: number }> = [];
for (let index = 0; index < CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS.length; index++) {
highlightBackground.push({
color: index === 2 ? '#00ff00' : '#ff0000',
alpha: index === 2 ? 0.5 : 1,
});
}
themeService.setTheme({
...themeService.getCurrentTheme(),
highlight: {
background: highlightBackground,
},
} as never);
return {
commandService,
service: injector.get(SheetsCrosshairHighlightService),
};
}
describe('crosshair highlight operations', () => {
let commandService: ICommandService;
let service: SheetsCrosshairHighlightService;
beforeEach(() => {
const testBed = createOperationTestBed();
commandService = testBed.commandService;
service = testBed.service;
});
it('toggles crosshair highlighting for the current sheet view', async () => {
expect(service.enabled).toBe(false);
await commandService.executeCommand(ToggleCrosshairHighlightOperation.id);
expect(service.enabled).toBe(true);
await commandService.executeCommand(ToggleCrosshairHighlightOperation.id);
expect(service.enabled).toBe(false);
});
it('reports whether explicit enable and disable commands actually changed the state', async () => {
await expect(commandService.executeCommand(EnableCrosshairHighlightOperation.id)).resolves.toBe(true);
await expect(commandService.executeCommand(EnableCrosshairHighlightOperation.id)).resolves.toBe(false);
expect(service.enabled).toBe(true);
await expect(commandService.executeCommand(DisableCrosshairHighlightOperation.id)).resolves.toBe(true);
await expect(commandService.executeCommand(DisableCrosshairHighlightOperation.id)).resolves.toBe(false);
expect(service.enabled).toBe(false);
});
it('enables crosshair highlighting when the user chooses a highlight color', async () => {
const result = await commandService.executeCommand(SetCrosshairHighlightColorOperation.id, {
value: CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[1],
});
expect(result).toBe(true);
expect(service.enabled).toBe(true);
await expect(firstValueFrom(service.color$)).resolves.toBe('rgba(0,255,0,0.5)');
});
});

View File

@@ -0,0 +1,90 @@
/**
* 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 type { IOperation } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { SheetsCrosshairHighlightService } from '../../services/crosshair.service';
/**
* A {@link CommandType.OPERATION} to toggle the crosshair highlight.
*/
export const ToggleCrosshairHighlightOperation: IOperation = {
id: 'sheet.operation.toggle-crosshair-highlight',
type: CommandType.OPERATION,
handler(accessor) {
const service = accessor.get(SheetsCrosshairHighlightService);
const turnedOn = service.enabled;
service.setEnabled(!turnedOn);
return true;
},
};
/**
* A {@link CommandType.OPERATION} to set the crosshair highlight color theme token.
* @property {string} value - The theme token path to set.
*/
export interface ISetCrosshairHighlightColorOperationParams {
value: string;
}
/**
* A {@link CommandType.OPERATION} to set the crosshair highlight color. If the crosshair highlight is not enabled,
* it will be enabled.
*/
export const SetCrosshairHighlightColorOperation: IOperation<ISetCrosshairHighlightColorOperationParams> = {
id: 'sheet.operation.set-crosshair-highlight-color',
type: CommandType.OPERATION,
handler(accessor, { value }) {
const service = accessor.get(SheetsCrosshairHighlightService);
if (!service.enabled) service.setEnabled(true);
service.setColor(value);
return true;
},
};
/**
* A {@link CommandType.OPERATION} to enable the crosshair highlight.
*/
export const EnableCrosshairHighlightOperation: IOperation = {
id: 'sheet.operation.enable-crosshair-highlight',
type: CommandType.OPERATION,
handler(accessor) {
const service = accessor.get(SheetsCrosshairHighlightService);
if (service.enabled) {
return false;
}
service.setEnabled(true);
return true;
},
};
/**
* A {@link CommandType.OPERATION} to disable the crosshair highlight.
*/
export const DisableCrosshairHighlightOperation: IOperation = {
id: 'sheet.operation.disable-crosshair-highlight',
type: CommandType.OPERATION,
handler(accessor) {
const service = accessor.get(SheetsCrosshairHighlightService);
if (!service.enabled) {
return false;
}
service.setEnabled(false);
return true;
},
};

View File

@@ -0,0 +1,27 @@
/**
* 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 type { MenuConfig } from '@univerjs/ui';
export const SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_CONFIG_KEY = 'sheets-crosshair-highlight.config';
export const configSymbol = Symbol(SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsCrosshairHighlightConfig {
menu?: MenuConfig;
}
export const defaultPluginConfig: IUniverSheetsCrosshairHighlightConfig = {};

View File

@@ -0,0 +1,18 @@
/**
* 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.
*/
export const SHEETS_CROSSHAIR_HIGHLIGHT_Z_INDEX = 1;
export const SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_NAME = 'SHEET_CROSSHAIR_HIGHLIGHT_PLUGIN';

View File

@@ -0,0 +1,49 @@
/**
* 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 { Disposable, Inject } from '@univerjs/core';
import { CrossHighlightingIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { CROSSHAIR_HIGHLIGHT_OVERLAY_COMPONENT } from '../menu/crosshair.menu';
import { MOBILE_CROSSHAIR_HIGHLIGHT_PANEL } from '../menu/mobile-crosshair.menu';
import { CrosshairOverlay } from '../views/components/CrosshairHighlight';
import { MobileCrosshairHighlightPanel } from '../views/mobile/MobileCrosshairHighlightPanel';
export class ComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(IconManager) private readonly _iconManager: IconManager
) {
super();
this._registerComponents();
this._registerIcons();
}
private _registerComponents(): void {
this.disposeWithMe(this._componentManager.register(CROSSHAIR_HIGHLIGHT_OVERLAY_COMPONENT, CrosshairOverlay));
this.disposeWithMe(this._componentManager.register(
MOBILE_CROSSHAIR_HIGHLIGHT_PANEL,
MobileCrosshairHighlightPanel
));
}
private _registerIcons(): void {
this.disposeWithMe(this._iconManager.register({
CrossHighlightingIcon,
}));
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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 { Disposable, ICommandService } from '@univerjs/core';
import { IMenuManagerService } from '@univerjs/ui';
import {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
ToggleCrosshairHighlightOperation,
} from '../commands/operations/operation';
import { menuSchema } from '../menu/schema';
export class SheetsCrosshairHighlightController extends Disposable {
constructor(
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@ICommandService private readonly _cmdSrv: ICommandService
) {
super();
this._initCommands();
this._initMenus();
}
private _initCommands(): void {
([
ToggleCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
EnableCrosshairHighlightOperation,
DisableCrosshairHighlightOperation,
]).forEach((c) => this._cmdSrv.registerCommand(c));
}
private _initMenus(): void {
this._menuManagerService.mergeMenu(menuSchema);
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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 { Disposable, ICommandService } from '@univerjs/core';
import { IMenuManagerService } from '@univerjs/ui';
import {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
ToggleCrosshairHighlightOperation,
} from '../../commands/operations/operation';
import { mobileMenuSchema } from '../../menu/mobile-schema';
export class SheetsCrosshairHighlightMobileController extends Disposable {
constructor(
@ICommandService commandService: ICommandService,
@IMenuManagerService menuManagerService: IMenuManagerService
) {
super();
[
ToggleCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
EnableCrosshairHighlightOperation,
DisableCrosshairHighlightOperation,
].forEach((command) => this.disposeWithMe(commandService.registerCommand(command)));
menuManagerService.mergeMenu(mobileMenuSchema);
}
}

View File

@@ -0,0 +1,112 @@
/**
* 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 type { IDisposable } from '@univerjs/core';
import { ICommandService, Injector } from '@univerjs/core';
import { FEventName, FUniver } from '@univerjs/core/facade';
import {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SheetsCrosshairHighlightService,
ToggleCrosshairHighlightOperation,
} from '@univerjs/sheets-crosshair-highlight';
import { describe, expect, it, vi } from 'vitest';
import { FSheetsCrosshairHighlightEventNameMixin } from '../f-event';
import { FUniverSheetsCrosshairHighlightMixin } from '../f-univer';
class TestCommandService {
readonly callbacks: Array<(commandInfo: { id: string }) => void> = [];
readonly executedCommandIds: string[] = [];
onCommandExecuted(callback: (commandInfo: { id: string }) => void): IDisposable {
this.callbacks.push(callback);
return { dispose: () => undefined };
}
syncExecuteCommand(id: string): boolean {
this.executedCommandIds.push(id);
return true;
}
}
class TestCrosshairHighlightService {
enabled = true;
}
describe('crosshair facade', () => {
it('emits the enabled-change event for crosshair commands on the active sheet', () => {
const injector = new Injector();
injector.add([ICommandService, { useClass: TestCommandService as never }]);
injector.add([SheetsCrosshairHighlightService, { useClass: TestCrosshairHighlightService as never }]);
const commandService = injector.get(ICommandService) as unknown as TestCommandService;
const fireEvent = vi.fn();
const registerEventHandler = vi.fn((_eventName: string, setup: () => unknown) => setup());
const thisArg = {
Event: FEventName.get(),
_injector: injector,
_commandService: commandService,
getActiveSheet: vi.fn(() => ({
workbook: { id: 'workbook-1' },
worksheet: { id: 'sheet-1' },
})),
getCrosshairHighlightEnabled: () => FUniverSheetsCrosshairHighlightMixin.prototype.getCrosshairHighlightEnabled.call(thisArg as never),
fireEvent,
registerEventHandler,
disposeWithMe: vi.fn(),
};
FUniverSheetsCrosshairHighlightMixin.prototype._initialize.call(thisArg as never, injector);
expect(registerEventHandler).toHaveBeenCalledWith('CrosshairHighlightEnabledChanged', expect.any(Function));
commandService.callbacks[0]({ id: EnableCrosshairHighlightOperation.id });
commandService.callbacks[0]({ id: DisableCrosshairHighlightOperation.id });
commandService.callbacks[0]({ id: ToggleCrosshairHighlightOperation.id });
commandService.callbacks[0]({ id: 'unrelated-command' });
expect(fireEvent).toHaveBeenCalledTimes(3);
expect(fireEvent).toHaveBeenLastCalledWith('CrosshairHighlightEnabledChanged', {
enabled: true,
workbook: { id: 'workbook-1' },
worksheet: { id: 'sheet-1' },
});
thisArg.getActiveSheet.mockReturnValue(undefined as never);
commandService.callbacks[0]({ id: EnableCrosshairHighlightOperation.id });
expect(fireEvent).toHaveBeenCalledTimes(3);
});
it('updates crosshair state through commands and exposes the event name on real facade classes', () => {
const injector = new Injector();
injector.add([ICommandService, { useClass: TestCommandService as never }]);
injector.add([SheetsCrosshairHighlightService, { useClass: TestCrosshairHighlightService as never }]);
const commandService = injector.get(ICommandService) as unknown as TestCommandService;
const thisArg = {
_injector: injector,
_commandService: commandService,
};
expect(FUniverSheetsCrosshairHighlightMixin.prototype.setCrosshairHighlightEnabled.call(thisArg as never, true)).toBe(thisArg);
expect(FUniverSheetsCrosshairHighlightMixin.prototype.setCrosshairHighlightEnabled.call(thisArg as never, false)).toBe(thisArg);
expect(commandService.executedCommandIds).toEqual([
EnableCrosshairHighlightOperation.id,
DisableCrosshairHighlightOperation.id,
]);
expect(FUniverSheetsCrosshairHighlightMixin.prototype.getCrosshairHighlightEnabled.call(thisArg as never)).toBe(true);
expect(FEventName.get().CrosshairHighlightEnabledChanged).toBe('CrosshairHighlightEnabledChanged');
expect(Object.create(FSheetsCrosshairHighlightEventNameMixin.prototype).CrosshairHighlightEnabledChanged).toBe('CrosshairHighlightEnabledChanged');
expect(typeof FUniver.prototype.setCrosshairHighlightEnabled).toBe('function');
});
});

View File

@@ -0,0 +1,75 @@
/**
* 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 type { IEventBase } from '@univerjs/core/facade';
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
import { FEventName } from '@univerjs/core/facade';
export interface IFSheetsCrosshairHighlightEventNameMixin {
/**
* Triggered when the crosshair highlight is enabled or disabled.
* @see {@link ICrosshairHighlightEnabledChangedEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.CrosshairHighlightEnabledChanged, (params) => {
* const { enabled, workbook, worksheet } = params;
* console.log(params);
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly CrosshairHighlightEnabledChanged: 'CrosshairHighlightEnabledChanged';
}
/**
* @ignore
*/
export class FSheetsCrosshairHighlightEventNameMixin extends FEventName implements IFSheetsCrosshairHighlightEventNameMixin {
override get CrosshairHighlightEnabledChanged(): 'CrosshairHighlightEnabledChanged' {
return 'CrosshairHighlightEnabledChanged';
}
}
export interface ICrosshairHighlightEnabledChangedEventParams extends IEventBase {
/**
* Whether the crosshair highlight is enabled.
*/
enabled: boolean;
/**
* The workbook that the crosshair highlight is enabled in.
*/
workbook: FWorkbook;
/**
* The worksheet that the crosshair highlight is enabled in.
*/
worksheet: FWorksheet;
}
/**
* @ignore
*/
export interface ISheetsCrosshairHighlightEventParamConfig {
CrosshairHighlightEnabledChanged: ICrosshairHighlightEnabledChangedEventParams;
}
FEventName.extend(FSheetsCrosshairHighlightEventNameMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEventName extends IFSheetsCrosshairHighlightEventNameMixin { }
interface IEventParamConfig extends ISheetsCrosshairHighlightEventParamConfig { }
}

View File

@@ -0,0 +1,106 @@
/**
* 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 type { Injector } from '@univerjs/core';
import type { ICrosshairHighlightEnabledChangedEventParams } from './f-event';
import { ICommandService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SheetsCrosshairHighlightService,
ToggleCrosshairHighlightOperation,
} from '@univerjs/sheets-crosshair-highlight';
/**
* @ignore
*/
export interface IFUniverSheetsCrosshairHighlightMixin {
/**
* Enable or disable crosshair highlight.
* @param {boolean} enabled - Whether to enable the crosshair highlight
* @returns {FUniver} The FUniver instance for chaining
* @example
* ```ts
* univerAPI.setCrosshairHighlightEnabled(true);
* ```
*/
setCrosshairHighlightEnabled(enabled: boolean): FUniver;
/**
* Get whether the crosshair highlight is enabled.
* @returns {boolean} Whether the crosshair highlight is enabled
* @example
* ```ts
* console.log(univerAPI.getCrosshairHighlightEnabled());
* ```
*/
getCrosshairHighlightEnabled(): boolean;
}
/**
* @ignore
*/
export class FUniverSheetsCrosshairHighlightMixin extends FUniver implements IFUniverSheetsCrosshairHighlightMixin {
/**
* @ignore
*/
override _initialize(injector: Injector): void {
const commandService = injector.get(ICommandService);
this.disposeWithMe(
this.registerEventHandler(
this.Event.CrosshairHighlightEnabledChanged,
() => commandService.onCommandExecuted((commandInfo) => {
if (
commandInfo.id === EnableCrosshairHighlightOperation.id ||
commandInfo.id === DisableCrosshairHighlightOperation.id ||
commandInfo.id === ToggleCrosshairHighlightOperation.id
) {
const activeSheet = this.getActiveSheet();
if (!activeSheet) return;
const eventParams: ICrosshairHighlightEnabledChangedEventParams = {
enabled: this.getCrosshairHighlightEnabled(),
...activeSheet,
};
this.fireEvent(this.Event.CrosshairHighlightEnabledChanged, eventParams);
}
})
)
);
}
override setCrosshairHighlightEnabled(enabled: boolean): FUniver {
if (enabled) {
this._commandService.syncExecuteCommand(EnableCrosshairHighlightOperation.id);
} else {
this._commandService.syncExecuteCommand(DisableCrosshairHighlightOperation.id);
}
return this;
}
override getCrosshairHighlightEnabled(): boolean {
const crosshairHighlightService = this._injector.get(SheetsCrosshairHighlightService);
return crosshairHighlightService.enabled;
}
}
FUniver.extend(FUniverSheetsCrosshairHighlightMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FUniver extends IFUniverSheetsCrosshairHighlightMixin { }
}

View File

@@ -0,0 +1,21 @@
/**
* 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 './f-univer';
import './f-event';
export type * from './f-event';
export type * from './f-univer';

View File

@@ -0,0 +1,2 @@
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,30 @@
/**
* 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 './global.css';
export {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
ToggleCrosshairHighlightOperation,
} from './commands/operations/operation';
export type { ISetCrosshairHighlightColorOperationParams } from './commands/operations/operation';
export type { IUniverSheetsCrosshairHighlightConfig } from './config/config';
export { menuSchema as SheetsCrosshairHighlightMenuSchema } from './menu/schema';
export { UniverSheetsCrosshairHighlightMobileUIPlugin } from './mobile-plugin';
export { UniverSheetsCrosshairHighlightPlugin } from './plugin';
export { SheetsCrosshairHighlightService } from './services/crosshair.service';

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'تسليط الضوء على التقاطع',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Ressaltat de creu',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Fadenkreuz-Hervorhebung',
},
},
};
export default locale;

View File

@@ -0,0 +1,25 @@
/**
* 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.
*/
const locale = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Crosshair Highlight',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Resaltado de cruz',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'برجسته کردن خط نشانه',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Surlignage du réticule',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Sorot Penyilang',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Evidenziazione mirino',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: '十字線を強調表示',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: '십자선 강조',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Podświetlenie krzyżowe',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Destaque de Mira Cruzada',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Перекрестный выделение',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Zvýraznenie krížika',
},
},
};
export default locale;

View File

@@ -0,0 +1,20 @@
/**
* 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 type { LocaleLeafKeys } from '@univerjs/core';
import type enUS from './en-US';
export type LocaleKey = LocaleLeafKeys<typeof enUS>;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: 'Đánh dấu chéo',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: '十字高亮',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: '十字高亮',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* 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 type enUS from './en-US';
const locale: typeof enUS = {
'sheets-crosshair-highlight': {
button: {
tooltip: '十字高亮',
},
},
};
export default locale;

View File

@@ -0,0 +1,52 @@
/**
* 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 type { IAccessor } from '@univerjs/core';
import type { IMenuSelectorItem } from '@univerjs/ui';
import type { LocaleKey } from '../locale/types';
import { UniverInstanceType } from '@univerjs/core';
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
import {
SetCrosshairHighlightColorOperation,
ToggleCrosshairHighlightOperation,
} from '../commands/operations/operation';
import { SheetsCrosshairHighlightService } from '../services/crosshair.service';
export const CROSSHAIR_HIGHLIGHT_OVERLAY_COMPONENT = 'CROSSHAIR_HIGHLIGHT_OVERLAY_COMPONENT';
export function CrosshairHighlightMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<LocaleKey> {
const crosshairHighlightService = accessor.get(SheetsCrosshairHighlightService);
return {
id: ToggleCrosshairHighlightOperation.id,
tooltip: 'sheets-crosshair-highlight.button.tooltip',
type: MenuItemType.BUTTON_SELECTOR,
icon: 'CrossHighlightingIcon',
selections: [
{
label: {
name: CROSSHAIR_HIGHLIGHT_OVERLAY_COMPONENT,
hoverable: false,
selectable: false,
props: { embedded: true },
},
},
],
selectionsCommandId: SetCrosshairHighlightColorOperation.id,
activated$: crosshairHighlightService.enabled$,
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
};
}

View File

@@ -0,0 +1,44 @@
/**
* 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 type { IAccessor } from '@univerjs/core';
import type { IMenuSelectorItem } from '@univerjs/ui';
import type { LocaleKey } from '../locale/types';
import { UniverInstanceType } from '@univerjs/core';
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
export const MOBILE_CROSSHAIR_HIGHLIGHT_MENU_ID = 'sheet.menu.mobile.crosshair-highlight';
export const MOBILE_CROSSHAIR_HIGHLIGHT_PANEL = 'MOBILE_CROSSHAIR_HIGHLIGHT_PANEL';
export function MobileCrosshairHighlightMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<LocaleKey> {
return {
id: MOBILE_CROSSHAIR_HIGHLIGHT_MENU_ID,
type: MenuItemType.SELECTOR,
icon: 'CrossHighlightingIcon',
title: 'sheets-crosshair-highlight.button.tooltip',
tooltip: 'sheets-crosshair-highlight.button.tooltip',
selections: [
{
label: {
name: MOBILE_CROSSHAIR_HIGHLIGHT_PANEL,
hoverable: false,
selectable: false,
},
},
],
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
};
}

View File

@@ -0,0 +1,31 @@
/**
* 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 type { MenuSchemaType } from '@univerjs/ui';
import { RibbonDataGroup } from '@univerjs/ui';
import {
MOBILE_CROSSHAIR_HIGHLIGHT_MENU_ID,
MobileCrosshairHighlightMenuItemFactory,
} from './mobile-crosshair.menu';
export const mobileMenuSchema: MenuSchemaType = {
[RibbonDataGroup.ORGANIZATION]: {
[MOBILE_CROSSHAIR_HIGHLIGHT_MENU_ID]: {
order: 21,
menuItemFactory: MobileCrosshairHighlightMenuItemFactory,
},
},
};

View File

@@ -0,0 +1,38 @@
/**
* 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 type { MenuSchemaType } from '@univerjs/ui';
import { ContextMenuGroup, ContextMenuPosition, RibbonViewGroup } from '@univerjs/ui';
import { ToggleCrosshairHighlightOperation } from '../commands/operations/operation';
import { CrosshairHighlightMenuItemFactory } from './crosshair.menu';
export const menuSchema: MenuSchemaType = {
[RibbonViewGroup.VISIBILITY]: {
[ToggleCrosshairHighlightOperation.id]: {
order: 1,
gridLayout: { row: 2, column: 1, showLabel: true },
menuItemFactory: CrosshairHighlightMenuItemFactory,
},
},
[ContextMenuPosition.FOOTER_MENU]: {
[ContextMenuGroup.OTHERS]: {
[ToggleCrosshairHighlightOperation.id]: {
order: 0,
menuItemFactory: CrosshairHighlightMenuItemFactory,
},
},
},
};

View File

@@ -0,0 +1,67 @@
/**
* 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 type { Dependency } from '@univerjs/core';
import type { IUniverSheetsCrosshairHighlightConfig } from './config/config';
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
import { UniverMobileUIPlugin } from '@univerjs/ui';
import pkg from '../package.json';
import { defaultPluginConfig, SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_CONFIG_KEY } from './config/config';
import { SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_NAME } from './const';
import { ComponentsController } from './controllers/components.controller';
import { SheetsCrosshairHighlightMobileController } from './controllers/mobile/crosshair.controller';
import { SheetsCrosshairHighlightService } from './services/crosshair.service';
import { SheetCrosshairHighlightRenderController } from './views/widgets/crosshair-highlight.render-controller';
@DependentOn(UniverRenderEnginePlugin, UniverSheetsPlugin, UniverSheetsMobileUIPlugin, UniverMobileUIPlugin)
export class UniverSheetsCrosshairHighlightMobileUIPlugin extends Plugin {
static override pluginName = SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_NAME;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsCrosshairHighlightConfig> = defaultPluginConfig,
@Inject(Injector) protected readonly _injector: Injector,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@IConfigService private readonly _configService: IConfigService
) {
super();
const { ...rest } = merge({}, defaultPluginConfig, this._config);
this._configService.setConfig(SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
([
[ComponentsController],
[SheetsCrosshairHighlightService],
[SheetsCrosshairHighlightMobileController],
] as Dependency[]).forEach((dependency) => this._injector.add(dependency));
}
override onReady(): void {
this._injector.get(ComponentsController);
this._injector.get(SheetsCrosshairHighlightMobileController);
this.disposeWithMe(this._renderManagerService.registerRenderModule(
UniverInstanceType.UNIVER_SHEET,
[SheetCrosshairHighlightRenderController] as Dependency
));
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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 type { Dependency } from '@univerjs/core';
import type { IUniverSheetsCrosshairHighlightConfig } from './config/config';
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import pkg from '../package.json';
import { defaultPluginConfig, SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_CONFIG_KEY } from './config/config';
import { SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_NAME } from './const';
import { ComponentsController } from './controllers/components.controller';
import { SheetsCrosshairHighlightController } from './controllers/crosshair.controller';
import { SheetsCrosshairHighlightService } from './services/crosshair.service';
import { SheetCrosshairHighlightRenderController } from './views/widgets/crosshair-highlight.render-controller';
@DependentOn(UniverRenderEnginePlugin, UniverSheetsPlugin, UniverSheetsUIPlugin)
export class UniverSheetsCrosshairHighlightPlugin extends Plugin {
static override pluginName: string = SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_NAME;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsCrosshairHighlightConfig> = defaultPluginConfig,
@Inject(Injector) protected readonly _injector: Injector,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(SHEETS_CROSSHAIR_HIGHLIGHT_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
([
[ComponentsController],
[SheetsCrosshairHighlightService],
[SheetsCrosshairHighlightController],
] as Dependency[]).forEach((d) => this._injector.add(d));
}
override onReady(): void {
([
[SheetCrosshairHighlightRenderController],
] as Dependency[]).forEach((d) => this._injector.add(d));
this._injector.get(ComponentsController);
this._injector.get(SheetsCrosshairHighlightController);
this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetCrosshairHighlightRenderController] as Dependency);
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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 { Injector, ThemeService } from '@univerjs/core';
import { firstValueFrom, skip, take } from 'rxjs';
import { describe, expect, it } from 'vitest';
import { CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS, SheetsCrosshairHighlightService } from '../crosshair.service';
function createService(): { service: SheetsCrosshairHighlightService; themeService: ThemeService } {
const injector = new Injector();
injector.add([ThemeService]);
injector.add([SheetsCrosshairHighlightService]);
return {
service: injector.get(SheetsCrosshairHighlightService),
themeService: injector.get(ThemeService),
};
}
describe('SheetsCrosshairHighlightService', () => {
it('tracks enabled state and resolves selected highlight colors from theme tokens', async () => {
const { service, themeService } = createService();
themeService.setTheme({
...themeService.getCurrentTheme(),
highlight: {
background: CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS.map((_path, index) => ({
color: index === 2 ? '#00ff00' : '#ff0000',
alpha: index === 2 ? 0.5 : 1,
})),
},
} as never);
const enabled = firstValueFrom(service.enabled$.pipe(skip(1), take(1)));
service.setEnabled(true);
expect(service.enabled).toBe(true);
await expect(enabled).resolves.toBe(true);
service.setColor(CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[1]);
await expect(firstValueFrom(service.color$)).resolves.toBe('rgba(0,255,0,0.5)');
});
});

View File

@@ -0,0 +1,78 @@
/**
* 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 type { Observable } from 'rxjs';
import { ColorKit, Disposable, Inject, ThemeService } from '@univerjs/core';
import { BehaviorSubject, combineLatest, map } from 'rxjs';
export const CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS = Array.from({ length: 16 }, (_, index) => `highlight.background.${index + 1}`);
export const DEFAULT_CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATH = CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[0];
interface IThemeColorWithAlpha {
color: string;
alpha: number;
}
export function resolveCrosshairHighlightColor(themeService: ThemeService, tokenPath: string): string {
const token = themeService.getColorFromTheme<IThemeColorWithAlpha | undefined>(tokenPath);
if (token == null || typeof token.color !== 'string' || typeof token.alpha !== 'number') {
throw new Error(`Theme token ${tokenPath} is required.`);
}
const color = themeService.getColorFromTheme<string | undefined>(token.color) ?? token.color;
return new ColorKit(color).setAlpha(token.alpha).toRgbString();
}
export function resolveCrosshairHighlightColors(themeService: ThemeService): string[] {
return CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS.map((tokenPath) => resolveCrosshairHighlightColor(themeService, tokenPath));
}
export class SheetsCrosshairHighlightService extends Disposable {
private readonly _enabled$ = new BehaviorSubject<boolean>(false);
readonly enabled$ = this._enabled$.asObservable();
get enabled(): boolean { return this._enabled$.getValue(); }
private readonly _colorToken$ = new BehaviorSubject<string>(DEFAULT_CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATH);
readonly colorToken$ = this._colorToken$.asObservable();
readonly color$: Observable<string>;
readonly highlightColor$: Observable<string>;
constructor(
@Inject(ThemeService) private readonly _themeService: ThemeService
) {
super();
this.color$ = combineLatest([this._colorToken$, this._themeService.currentTheme$]).pipe(
map(([tokenPath]) => resolveCrosshairHighlightColor(this._themeService, tokenPath))
);
this.highlightColor$ = this.color$;
}
override dispose(): void {
this._enabled$.complete();
this._colorToken$.complete();
}
setEnabled(value: boolean): void {
this._enabled$.next(value);
}
setColor(value: string): void {
this._colorToken$.next(value);
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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 type { IRange } from '@univerjs/core';
import { getIntersectRange, RANGE_TYPE, Rectangle } from '@univerjs/core';
export class CrossHairRangeCollection {
private _selectedRanges: IRange[] = [];
private _ranges: IRange[] = [];
public addRange(range: IRange) {
if (range.rangeType === RANGE_TYPE.COLUMN || range.rangeType === RANGE_TYPE.ROW || range.rangeType === RANGE_TYPE.ALL) {
return;
}
const intersects = this._getIntersects(range);
const splitRanges = this._getSplitRanges(range, intersects);
if (splitRanges.length > 0) {
this._ranges.push(...splitRanges);
}
}
public setSelectedRanges(selectedRange: IRange[]) {
this._selectedRanges = selectedRange;
}
private _getSplitRanges(range: IRange, intersects: IRange[]) {
let splitRanges: IRange[] = [range];
for (const intersect of intersects.concat(this._selectedRanges)) {
const newRanges = [];
for (const splitRange of splitRanges) {
const split = Rectangle.subtract(splitRange, intersect);
if (split && split.length > 0) {
newRanges.push(...split);
}
}
splitRanges = newRanges;
}
return splitRanges.filter((range) => range.startRow <= range.endRow && range.startColumn <= range.endColumn);
}
private _getIntersects(addRange: IRange) {
const intersects = [];
for (const range of this._ranges) {
const intersect = getIntersectRange(range, addRange);
if (intersect) {
intersects.push(intersect);
}
}
return intersects;
}
getRanges() {
return this._ranges;
}
reset() {
this._ranges = [];
this._selectedRanges = [];
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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 { ThemeService } from '@univerjs/core';
import { borderClassName, clsx } from '@univerjs/design';
import { useDependency, useObservable } from '@univerjs/ui';
import { useCallback } from 'react';
import { map } from 'rxjs';
import {
CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS,
resolveCrosshairHighlightColors,
SheetsCrosshairHighlightService,
} from '../../services/crosshair.service';
export interface ICrosshairOverlayProps {
onChange?: (value: string) => void;
}
export function CrosshairOverlay(props: ICrosshairOverlayProps) {
const { onChange } = props;
const crosshairSrv = useDependency(SheetsCrosshairHighlightService);
const themeService = useDependency(ThemeService);
const currentColor = useObservable(crosshairSrv.color$);
const colors = useObservable(
() => themeService.currentTheme$.pipe(map(() => resolveCrosshairHighlightColors(themeService))),
resolveCrosshairHighlightColors(themeService),
false,
[themeService]
);
const handleColorPicked = useCallback((tokenPath: string) => {
onChange?.(tokenPath);
}, [onChange]);
return (
<div className="univer-grid univer-grid-cols-8 univer-gap-x-2 univer-gap-y-3 univer-p-1.5">
{colors.map((color: string, index: number) => {
return (
<div
key={CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[index]}
className={clsx(`
hover:univer-ring-primary-600/40
univer-box-border univer-size-5 univer-cursor-pointer univer-rounded univer-ring-offset-1
univer-transition-shadow
hover:univer-ring-[1.5px]
`, borderClassName, {
'univer-ring-[1.5px] univer-ring-primary-600 hover:univer-ring-primary-600': color === currentColor,
})}
style={{ backgroundColor: color }}
onClick={() => handleColorPicked(CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[index])}
/>
);
})}
</div>
);
}

View File

@@ -0,0 +1,158 @@
/**
* 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 type { IDisposable } from '@univerjs/core';
import type { Root } from 'react-dom/client';
import { Injector, ThemeService, toDisposable } from '@univerjs/core';
import { RediContext } from '@univerjs/ui';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS,
SheetsCrosshairHighlightService,
} from '../../../services/crosshair.service';
import { CrosshairOverlay } from '../CrosshairHighlight';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
class TestState {
static pickedTokens: string[] = [];
static reset(): void {
this.pickedTokens = [];
}
}
function createOverlayTestBed() {
const injector = new Injector();
injector.add([ThemeService]);
injector.add([SheetsCrosshairHighlightService]);
const themeService = injector.get(ThemeService);
const highlightBackground = Object.fromEntries(CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS.map((path, index) => [
path.replace('highlight.background.', ''),
{
color: index === 0 ? '#010203' : index === 1 ? '#040506' : `#${String(index + 10).padStart(2, '0')}0a0b`,
alpha: index === 1 ? 0.15 : 0.3,
},
]));
themeService.setTheme({
...themeService.getCurrentTheme(),
highlight: {
background: highlightBackground,
},
} as never);
return {
injector,
service: injector.get(SheetsCrosshairHighlightService),
};
}
function renderOverlay(injector: Injector) {
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(
<RediContext.Provider value={{ injector }}>
<CrosshairOverlay onChange={(token) => TestState.pickedTokens.push(token)} />
</RediContext.Provider>
);
});
return { container, root };
}
function getColorCells(container: HTMLElement): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>('div[style*="background-color"]'));
}
function hasClassToken(element: HTMLElement, token: string): boolean {
return element.className.split(/\s+/).includes(token);
}
function teardown(root?: Root, container?: HTMLElement): IDisposable {
return toDisposable(() => {
if (root) {
act(() => root.unmount());
}
container?.remove();
});
}
describe('CrosshairOverlay', () => {
let disposable: IDisposable | undefined;
beforeEach(() => {
TestState.reset();
});
afterEach(() => {
disposable?.dispose();
disposable = undefined;
});
it('offers theme-backed highlight colors and reports the picked token', () => {
const testBed = createOverlayTestBed();
const rendered = renderOverlay(testBed.injector);
disposable = teardown(rendered.root, rendered.container);
const cells = getColorCells(rendered.container);
expect(cells).toHaveLength(CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS.length);
expect(cells[0].style.backgroundColor).toBe('rgba(1, 2, 3, 0.3)');
expect(cells[1].style.backgroundColor).toBe('rgba(4, 5, 6, 0.15)');
act(() => {
cells[1].dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(TestState.pickedTokens).toEqual([CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[1]]);
});
it('marks the service-selected color as the active swatch', () => {
const testBed = createOverlayTestBed();
testBed.service.setColor(CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[1]);
const rendered = renderOverlay(testBed.injector);
disposable = teardown(rendered.root, rendered.container);
const cells = getColorCells(rendered.container);
expect(hasClassToken(cells[1], 'univer-ring-primary-600')).toBe(true);
expect(hasClassToken(cells[0], 'univer-ring-primary-600')).toBe(false);
});
it('moves the active swatch when the service color changes after render', () => {
const testBed = createOverlayTestBed();
const rendered = renderOverlay(testBed.injector);
disposable = teardown(rendered.root, rendered.container);
let cells = getColorCells(rendered.container);
expect(hasClassToken(cells[0], 'univer-ring-primary-600')).toBe(true);
expect(hasClassToken(cells[2], 'univer-ring-primary-600')).toBe(false);
act(() => {
testBed.service.setColor(CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[2]);
});
cells = getColorCells(rendered.container);
expect(hasClassToken(cells[0], 'univer-ring-primary-600')).toBe(false);
expect(hasClassToken(cells[2], 'univer-ring-primary-600')).toBe(true);
});
});

View File

@@ -0,0 +1,100 @@
/**
* 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 type { LocaleKey } from '../../locale/types';
import { ICommandService, LocaleService, ThemeService } from '@univerjs/core';
import { clsx, resetButtonClassName, Switch } from '@univerjs/design';
import { useDependency, useObservable } from '@univerjs/ui';
import { map } from 'rxjs';
import {
DisableCrosshairHighlightOperation,
EnableCrosshairHighlightOperation,
SetCrosshairHighlightColorOperation,
} from '../../commands/operations/operation';
import {
CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS,
resolveCrosshairHighlightColors,
SheetsCrosshairHighlightService,
} from '../../services/crosshair.service';
export function MobileCrosshairHighlightPanel() {
const commandService = useDependency(ICommandService);
const crosshairHighlightService = useDependency(SheetsCrosshairHighlightService);
const localeService = useDependency(LocaleService);
const themeService = useDependency(ThemeService);
const enabled = useObservable(crosshairHighlightService.enabled$, false);
const currentColor = useObservable(crosshairHighlightService.color$);
const colors = useObservable(
() => themeService.currentTheme$.pipe(map(() => resolveCrosshairHighlightColors(themeService))),
resolveCrosshairHighlightColors(themeService),
false,
[themeService]
);
return (
<div className="univer-grid univer-w-full univer-gap-4 univer-p-4">
<div className="univer-flex univer-min-h-8 univer-items-center univer-gap-4">
<span
className="
univer-min-w-0 univer-flex-1 univer-text-base univer-font-medium univer-text-gray-900
dark:!univer-text-gray-100
"
>
{localeService.t<LocaleKey>('sheets-crosshair-highlight.button.tooltip')}
</span>
<Switch
defaultChecked={enabled}
onChange={(checked) => {
const operation = checked
? EnableCrosshairHighlightOperation
: DisableCrosshairHighlightOperation;
commandService.executeCommand(operation.id).catch(() => undefined);
}}
/>
</div>
<div className="univer-grid univer-grid-cols-8 univer-gap-3">
{colors.map((color, index) => {
const tokenPath = CROSSHAIR_HIGHLIGHT_COLOR_THEME_PATHS[index];
const selected = color === currentColor;
return (
<button
key={tokenPath}
type="button"
data-color-token={tokenPath}
title={color}
aria-pressed={selected}
className={clsx(resetButtonClassName, `
univer-box-border univer-size-8 univer-rounded-lg univer-border univer-border-solid
univer-border-gray-200 univer-ring-offset-2
active:univer-opacity-70
dark:!univer-border-gray-600
`, {
'univer-ring-2 univer-ring-primary-600': selected,
})}
style={{ backgroundColor: color }}
onClick={() => {
commandService.executeCommand(SetCrosshairHighlightColorOperation.id, {
value: tokenPath,
}).catch(() => undefined);
}}
/>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,80 @@
/**
* 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 { describe, expect, it } from 'vitest';
import { SheetCrossHairHighlightShape } from '../crosshair-highlight-shape';
class TestCanvasContext {
fillStyle = '';
readonly rects: Array<{ left: number; top: number; width: number; height: number }> = [];
fillCount = 0;
save(): void {}
restore(): void {}
beginPath(): void {}
closePath(): void {}
rect(left: number, top: number, width: number, height: number): void {
this.rects.push({ left, top, width, height });
}
fill(): void {
this.fillCount += 1;
}
}
function drawShape(shape: SheetCrossHairHighlightShape, context: TestCanvasContext): void {
(shape as unknown as { _draw: (ctx: TestCanvasContext) => void })._draw(context);
}
describe('SheetCrossHairHighlightShape', () => {
it('draws the highlight rectangle with its configured rgba fill', () => {
const shape = new SheetCrossHairHighlightShape('shape-1', {
color: { r: 1, g: 2, b: 3, a: 0.4 },
width: 20,
height: 10,
left: 0,
top: 0,
zIndex: 1,
evented: false,
});
shape.setShapeProps({ width: 30, height: 15 });
const context = new TestCanvasContext();
drawShape(shape, context);
expect(context.rects).toEqual([{ left: 0, top: 0, width: 30, height: 15 }]);
expect(context.fillStyle).toBe('rgba(1, 2, 3, 0.4)');
expect(context.fillCount).toBe(1);
});
it('uses the default half opacity when the color has no alpha channel', () => {
const shape = new SheetCrossHairHighlightShape('shape-default-alpha');
shape.setShapeProps({
color: { r: 7, g: 8, b: 9 },
width: 1,
height: 1,
});
const context = new TestCanvasContext();
drawShape(shape, context);
expect(context.fillStyle).toBe('rgba(7, 8, 9, 0.5)');
});
});

View File

@@ -0,0 +1,236 @@
/**
* 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 type { IRange } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import { BehaviorSubject, Subject } from 'rxjs';
import { describe, expect, it } from 'vitest';
import { SheetCrossHairHighlightShape } from '../crosshair-highlight-shape';
import { SheetCrosshairHighlightRenderController } from '../crosshair-highlight.render-controller';
class TestSheet {
getRowCount(): number {
return 20;
}
getColumnCount(): number {
return 10;
}
}
class TestWorkbook {
readonly activeSheet$ = new BehaviorSubject(0);
readonly sheet = new TestSheet();
getActiveSheet(): TestSheet {
return this.sheet;
}
}
class TestScene {
readonly objects: SheetCrossHairHighlightShape[] = [];
readonly dirtyRequests: boolean[] = [];
addObject(object: SheetCrossHairHighlightShape): void {
this.objects.push(object);
}
makeDirty(value: boolean): void {
this.dirtyRequests.push(value);
}
}
class TestSkeleton {
getCellWithCoordByIndex(row: number, column: number) {
return {
startX: column * 100,
startY: row * 20,
endX: column * 100 + 100,
endY: row * 20 + 20,
};
}
}
class TestSkeletonManagerService {
readonly skeleton = new TestSkeleton();
readonly currentSkeleton$ = new BehaviorSubject(this.skeleton);
getCurrentSkeleton(): TestSkeleton {
return this.skeleton;
}
}
class TestSelectionsService {
readonly selectionMoveStart$ = new Subject<ISelectionWithStyle[]>();
readonly selectionMoving$ = new Subject<ISelectionWithStyle[]>();
readonly selectionMoveEnd$ = new Subject<ISelectionWithStyle[]>();
readonly selectionSet$ = new Subject<ISelectionWithStyle[]>();
private _selections: ISelectionWithStyle[] = [];
setSelections(selections: ISelectionWithStyle[]): void {
this._selections = selections;
this.selectionSet$.next(selections);
}
getCurrentSelections(): ISelectionWithStyle[] {
return this._selections;
}
}
class TestCrosshairHighlightService {
readonly enabled$ = new BehaviorSubject(false);
readonly highlightColor$ = new BehaviorSubject('rgba(10,20,30,0.6)');
}
class TestContextService {
readonly refSelectionEnabled$ = new BehaviorSubject(false);
subscribeContextValue$() {
return this.refSelectionEnabled$.asObservable();
}
}
function createSelection(range: IRange): ISelectionWithStyle {
return { range } as ISelectionWithStyle;
}
function getShapeRects(scene: TestScene) {
return scene.objects.map((shape) => ({
left: shape.left,
top: shape.top,
width: shape.width,
height: shape.height,
}));
}
function getRenderedShapeCount(controller: SheetCrosshairHighlightRenderController): number {
return (controller as unknown as { _shapes: SheetCrossHairHighlightShape[] })._shapes.length;
}
describe('SheetCrosshairHighlightRenderController', () => {
it('renders crosshair rectangles from the active normal selection and clears them when disabled', async () => {
const workbook = new TestWorkbook();
const scene = new TestScene();
const skeletonManagerService = new TestSkeletonManagerService();
const selectionsService = new TestSelectionsService();
const refSelectionsService = new TestSelectionsService();
const crosshairHighlightService = new TestCrosshairHighlightService();
const contextService = new TestContextService();
selectionsService.setSelections([
createSelection({
startRow: 1,
endRow: 1,
startColumn: 1,
endColumn: 1,
}),
]);
const controller = new SheetCrosshairHighlightRenderController(
{
unit: workbook,
scene,
} as never,
skeletonManagerService as never,
selectionsService as never,
crosshairHighlightService as never,
contextService as never,
refSelectionsService as never
);
crosshairHighlightService.enabled$.next(true);
expect(getRenderedShapeCount(controller)).toBe(4);
expect(scene.objects.every((object) => object instanceof SheetCrossHairHighlightShape)).toBe(true);
expect(getShapeRects(scene)).toEqual([
{ left: 0, top: 20, width: 100, height: 20 },
{ left: 200, top: 20, width: 900, height: 20 },
{ left: 100, top: 0, width: 100, height: 20 },
{ left: 100, top: 40, width: 100, height: 380 },
]);
expect(scene.dirtyRequests).toEqual([true]);
crosshairHighlightService.enabled$.next(false);
expect(getRenderedShapeCount(controller)).toBe(0);
await controller.dispose();
});
it('switches to reference selections and ignores full-row or full-column selections', async () => {
const workbook = new TestWorkbook();
const scene = new TestScene();
const skeletonManagerService = new TestSkeletonManagerService();
const selectionsService = new TestSelectionsService();
const refSelectionsService = new TestSelectionsService();
const crosshairHighlightService = new TestCrosshairHighlightService();
const contextService = new TestContextService();
selectionsService.setSelections([
createSelection({
startRow: 2,
endRow: 2,
startColumn: 2,
endColumn: 2,
}),
]);
refSelectionsService.setSelections([
createSelection({
startRow: 0,
endRow: 19,
startColumn: 3,
endColumn: 3,
}),
createSelection({
startRow: 3,
endRow: 3,
startColumn: 4,
endColumn: 4,
}),
]);
const controller = new SheetCrosshairHighlightRenderController(
{
unit: workbook,
scene,
} as never,
skeletonManagerService as never,
selectionsService as never,
crosshairHighlightService as never,
contextService as never,
refSelectionsService as never
);
crosshairHighlightService.enabled$.next(true);
expect(getShapeRects(scene).slice(-4)).toEqual([
{ left: 0, top: 40, width: 200, height: 20 },
{ left: 300, top: 40, width: 800, height: 20 },
{ left: 200, top: 0, width: 100, height: 40 },
{ left: 200, top: 60, width: 100, height: 360 },
]);
contextService.refSelectionEnabled$.next(true);
expect(getShapeRects(scene).slice(-4)).toEqual([
{ left: 0, top: 60, width: 400, height: 20 },
{ left: 500, top: 60, width: 600, height: 20 },
{ left: 400, top: 0, width: 100, height: 60 },
{ left: 400, top: 80, width: 100, height: 340 },
]);
expect(getRenderedShapeCount(controller)).toBe(4);
await controller.dispose();
});
});

View File

@@ -0,0 +1,60 @@
/**
* 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 type { IRgbColor, Nullable } from '@univerjs/core';
import type { IShapeProps, UniverRenderingContext } from '@univerjs/engine-render';
import { Rect, Shape } from '@univerjs/engine-render';
export interface ISheetCrossHairHighlightShapeProps extends IShapeProps {
color: IRgbColor;
}
export class SheetCrossHairHighlightShape extends Shape<ISheetCrossHairHighlightShapeProps> {
// protected _showHighLight = false;
protected _color: Nullable<IRgbColor>;
constructor(key?: string, props?: ISheetCrossHairHighlightShapeProps) {
super(key, props);
if (props) {
this.setShapeProps(props);
}
}
setShapeProps(props: Partial<ISheetCrossHairHighlightShapeProps>): void {
if (typeof props.color !== 'undefined') {
this._color = props.color;
}
this.transformByState({
width: props.width!,
height: props.height!,
});
}
protected override _draw(ctx: CanvasRenderingContext2D): void {
const color = `rgba(${this._color!.r}, ${this._color!.g}, ${this._color!.b}, ${this._color?.a ?? 0.5})`;
Rect.drawWith(ctx as UniverRenderingContext, {
width: this.width,
height: this.height,
fill: color,
stroke: undefined,
strokeWidth: 0,
evented: false,
});
}
}

View File

@@ -0,0 +1,192 @@
/**
* 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 type { IRange, Nullable, Workbook, Worksheet } from '@univerjs/core';
import type { IRenderContext, IRenderModule, Scene, SpreadsheetSkeleton } from '@univerjs/engine-render';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import { ColorKit, Disposable, IContextService, Inject, RANGE_TYPE } from '@univerjs/core';
import { IRefSelectionsService, REF_SELECTIONS_ENABLED, SheetsSelectionsService } from '@univerjs/sheets';
import { getCoordByCell, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { combineLatest, map, merge, startWith, tap } from 'rxjs';
import { SHEETS_CROSSHAIR_HIGHLIGHT_Z_INDEX } from '../../const';
import { SheetsCrosshairHighlightService } from '../../services/crosshair.service';
import { CrossHairRangeCollection } from '../../util';
import { SheetCrossHairHighlightShape } from './crosshair-highlight-shape';
export class SheetCrosshairHighlightRenderController extends Disposable implements IRenderModule {
private _shapes: SheetCrossHairHighlightShape[] = [];
private _rangeCollection = new CrossHairRangeCollection();
private _color: string = 'rgba(255,0,0,0.5)';
constructor(
private readonly _context: IRenderContext<Workbook>,
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
@Inject(SheetsSelectionsService) private readonly _sheetsSelectionsService: SheetsSelectionsService,
@Inject(SheetsCrosshairHighlightService) private readonly _sheetsCrosshairHighlightService: SheetsCrosshairHighlightService,
@Inject(IContextService) private readonly _contextService: IContextService,
@IRefSelectionsService private readonly _refSelectionsService: SheetsSelectionsService
) {
super();
this._initRenderListener();
}
private _transformSelection(selectionData: Readonly<Nullable<ISelectionWithStyle[]>>, sheet: Worksheet): void {
if (!selectionData) {
return;
}
const rowCount = sheet.getRowCount();
const columnCount = sheet.getColumnCount();
const ranges = [];
// remove the selection that covers the whole row or column
for (const selection of selectionData) {
const { startRow, endRow, startColumn, endColumn } = selection.range;
if (endRow - startRow + 1 === rowCount || endColumn - startColumn + 1 === columnCount) {
continue;
}
ranges.push(selection.range);
}
this._rangeCollection.setSelectedRanges(ranges);
for (const range of ranges) {
this.addSelection(range, sheet);
}
}
private _initRenderListener(): void {
const workbook = this._context.unit;
this.disposeWithMe(combineLatest([
this._contextService.subscribeContextValue$(REF_SELECTIONS_ENABLED).pipe(startWith(false)),
this._sheetSkeletonManagerService.currentSkeleton$,
this._sheetsCrosshairHighlightService.enabled$,
this._sheetsCrosshairHighlightService.highlightColor$.pipe(tap((color) => (this._color = color))),
merge(
this._sheetsSelectionsService.selectionMoveStart$,
this._sheetsSelectionsService.selectionMoving$,
this._sheetsSelectionsService.selectionMoveEnd$,
this._sheetsSelectionsService.selectionSet$,
workbook.activeSheet$.pipe(map(() => this._sheetsSelectionsService.getCurrentSelections()))
),
merge(
this._refSelectionsService.selectionMoveStart$,
this._refSelectionsService.selectionMoving$,
this._refSelectionsService.selectionMoveEnd$,
this._sheetsSelectionsService.selectionSet$,
workbook.activeSheet$.pipe(map(() => this._refSelectionsService.getCurrentSelections()))
),
]).subscribe(([refSelectionEnabled, _, enabled, _color, normalSelections, refSelection]) => {
this._clear();
if (!enabled) return;
const selections = refSelectionEnabled ? refSelection : normalSelections;
this._rangeCollection.reset();
this._transformSelection(selections, workbook.getActiveSheet());
this.render(this._rangeCollection.getRanges());
}));
}
addSelection(range: IRange, sheet: Worksheet): void {
if (range.rangeType === RANGE_TYPE.COLUMN || range.rangeType === RANGE_TYPE.ROW || range.rangeType === RANGE_TYPE.ALL) {
return;
}
const maxRow = sheet.getRowCount();
const maxColumn = sheet.getColumnCount();
const { startRow, endRow, startColumn, endColumn } = range;
const left = {
startRow,
endRow,
startColumn: 0,
endColumn: startColumn - 1,
};
const right = {
startRow,
endRow,
startColumn: endColumn + 1,
endColumn: maxColumn,
};
const top = {
startRow: 0,
endRow: startRow - 1,
startColumn,
endColumn,
};
const bottom = {
startRow: endRow + 1,
endRow: maxRow,
startColumn,
endColumn,
};
for (const range of [left, right, top, bottom]) {
// test if the range is valid
if (range.startRow <= range.endRow && range.startColumn <= range.endColumn) {
this._rangeCollection.addRange(range);
}
}
}
private _clear(): void {
this._shapes.forEach((shape) => {
shape.dispose();
});
this._shapes = [];
}
private _addShapes(range: IRange, index: number, scene: Scene, skeleton: SpreadsheetSkeleton): void {
const { startRow, endRow, startColumn, endColumn } = range;
const startPosition = getCoordByCell(startRow, startColumn, scene, skeleton);
const endPosition = getCoordByCell(endRow, endColumn, scene, skeleton);
const { startX, startY } = startPosition;
const { endX, endY } = endPosition;
const width = endX - startX;
const height = endY - startY;
const shapeProps = {
left: startX,
top: startY,
color: new ColorKit(this._color).toRgb(),
width,
height,
zIndex: SHEETS_CROSSHAIR_HIGHLIGHT_Z_INDEX,
evented: false,
};
const currentShapes = new SheetCrossHairHighlightShape(`crosshair-${index}`, shapeProps);
this._shapes.push(currentShapes);
scene.addObject(currentShapes);
}
render(ranges: IRange[]): void {
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
if (!skeleton) {
return;
}
const { scene } = this._context;
this._clear();
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
this._addShapes(range, i, scene, skeleton);
}
scene.makeDirty(true);
}
override async dispose(): Promise<void> {
super.dispose();
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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 type { Config } from 'tailwindcss';
import preset from '@univerjs-infra/shared/tailwind';
const config: Config = {
presets: [preset],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
],
};
export default config;

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/node",
"compilerOptions": {
"rootDir": "src",
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": false,
"outDir": "lib/types"
},
"include": [
"src"
],
"exclude": [
"src/**/__tests__/**",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();