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
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:
41
packages/sheets-hyper-link-ui/README.md
Normal file
41
packages/sheets-hyper-link-ui/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/sheets-hyper-link-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-hyper-link-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-hyper-link-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-hyper-link-ui)
|
||||
|
||||
`@univerjs/sheets-hyper-link-ui` adds hyperlink menus, dialogs, rendering, and interaction UI for Univer Sheets.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-hyper-link-ui` | `UniverSheetsHyperLinkUi` | Yes | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-hyper-link-ui
|
||||
# or
|
||||
npm install @univerjs/sheets-hyper-link-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/sheets-hyper-link-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/sheets-hyper-link-ui/locale/en-US';
|
||||
import { UniverSheetsHyperLinkUIPlugin } from '@univerjs/sheets-hyper-link-ui';
|
||||
|
||||
univer.registerPlugin(UniverSheetsHyperLinkUIPlugin);
|
||||
|
||||
// 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-hyper-link-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
111
packages/sheets-hyper-link-ui/package.json
Normal file
111
packages/sheets-hyper-link-ui/package.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-hyper-link-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Hyperlink editing UI 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",
|
||||
"hyperlink",
|
||||
"link",
|
||||
"ui"
|
||||
],
|
||||
"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/docs": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/engine-formula": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-data-validation": "workspace:*",
|
||||
"@univerjs/sheets-formula-ui": "workspace:*",
|
||||
"@univerjs/sheets-hyper-link": "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"
|
||||
}
|
||||
}
|
||||
1
packages/sheets-hyper-link-ui/postcss.config.mjs
Normal file
1
packages/sheets-hyper-link-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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 { IOpenHyperLinkEditPanelOperationParams } from '../popup.operations';
|
||||
import { ICommandService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ISheetsHyperLinkPopupService } from '../../../services/popup.service';
|
||||
import { HyperLinkEditSourceType } from '../../../types/enums/edit-source';
|
||||
import {
|
||||
CloseHyperLinkPopupOperation,
|
||||
InsertHyperLinkOperation,
|
||||
InsertHyperLinkToolbarOperation,
|
||||
OpenHyperLinkEditPanelOperation,
|
||||
} from '../popup.operations';
|
||||
|
||||
function createAccessor(pairs: Array<[unknown, unknown]>) {
|
||||
const map = new Map<unknown, unknown>(pairs);
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (!map.has(token)) {
|
||||
throw new Error(`Unknown token: ${String(token)}`);
|
||||
}
|
||||
return map.get(token);
|
||||
},
|
||||
has(token: unknown) {
|
||||
return map.has(token);
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
function createWorksheet(cell: Record<string, unknown> | null = null) {
|
||||
return {
|
||||
getSheetId: () => 's1',
|
||||
getUnitId: () => 'u1',
|
||||
getCell: vi.fn(() => cell),
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkbook(worksheet: ReturnType<typeof createWorksheet>) {
|
||||
return {
|
||||
getUnitId: () => 'u1',
|
||||
getActiveSheet: () => worksheet,
|
||||
getSheetBySheetId: () => worksheet,
|
||||
};
|
||||
}
|
||||
|
||||
function createUniverInstanceService(options?: {
|
||||
workbook?: ReturnType<typeof createWorkbook> | null;
|
||||
}) {
|
||||
return {
|
||||
getCurrentUnitOfType: () => options?.workbook ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
describe('hyper-link popup operations', () => {
|
||||
it('starts add and edit flows through the popup service', () => {
|
||||
const startAddEditing = vi.fn();
|
||||
const startEditing = vi.fn();
|
||||
const popupService = { startAddEditing, startEditing };
|
||||
const accessor = createAccessor([[ISheetsHyperLinkPopupService, popupService]]);
|
||||
const startAddParams: IOpenHyperLinkEditPanelOperationParams = {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
row: 1,
|
||||
col: 2,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
};
|
||||
const startEditParams: IOpenHyperLinkEditPanelOperationParams = {
|
||||
...startAddParams,
|
||||
customRangeId: 'range-1',
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
};
|
||||
|
||||
expect(OpenHyperLinkEditPanelOperation.handler(accessor, undefined)).toBe(false);
|
||||
expect(OpenHyperLinkEditPanelOperation.handler(accessor, startAddParams)).toBe(true);
|
||||
expect(OpenHyperLinkEditPanelOperation.handler(accessor, startEditParams)).toBe(true);
|
||||
|
||||
expect(startAddEditing).toHaveBeenCalledTimes(1);
|
||||
expect(startEditing).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('closes the popup edit session', () => {
|
||||
const endEditing = vi.fn();
|
||||
const accessor = createAccessor([[ISheetsHyperLinkPopupService, { endEditing }]]);
|
||||
|
||||
expect(CloseHyperLinkPopupOperation.handler(accessor)).toBe(true);
|
||||
expect(endEditing).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('derives the correct edit source from the current sheet selection state', () => {
|
||||
const executeCommand = vi.fn(() => true);
|
||||
const worksheet = createWorksheet();
|
||||
const workbook = createWorkbook(worksheet);
|
||||
|
||||
const createInsertAccessor = (visible: boolean, hasSelection = true, activeWorkbook: ReturnType<typeof createWorkbook> | null = workbook) => createAccessor([
|
||||
[IUniverInstanceService, createUniverInstanceService({ workbook: activeWorkbook })],
|
||||
[ICommandService, { executeCommand }],
|
||||
[SheetsSelectionsService, { getCurrentLastSelection: () => (hasSelection ? { range: { startRow: 3, startColumn: 4 } } : null) }],
|
||||
[IEditorBridgeService, { isVisible: () => ({ visible }) }],
|
||||
]);
|
||||
|
||||
expect(InsertHyperLinkOperation.handler(createInsertAccessor(false))).toBe(true);
|
||||
expect(InsertHyperLinkOperation.handler(createInsertAccessor(true))).toBe(true);
|
||||
|
||||
expect(executeCommand).toHaveBeenNthCalledWith(1, OpenHyperLinkEditPanelOperation.id, expect.objectContaining({ type: HyperLinkEditSourceType.VIEWING }));
|
||||
expect(executeCommand).toHaveBeenNthCalledWith(2, OpenHyperLinkEditPanelOperation.id, expect.objectContaining({ type: HyperLinkEditSourceType.EDITING }));
|
||||
|
||||
expect(InsertHyperLinkOperation.handler(createInsertAccessor(false, true, null))).toBe(false);
|
||||
|
||||
expect(InsertHyperLinkOperation.handler(createInsertAccessor(false, false))).toBe(false);
|
||||
});
|
||||
|
||||
it('respects disabled cells and toggles between insert and close commands from the toolbar', () => {
|
||||
const executeCommand = vi.fn(() => true);
|
||||
const disabledWorksheet = createWorksheet({ f: '=A1' });
|
||||
const enabledWorksheet = createWorksheet({ v: 'cell' });
|
||||
|
||||
expect(InsertHyperLinkToolbarOperation.handler(createAccessor([
|
||||
[IUniverInstanceService, createUniverInstanceService({ workbook: createWorkbook(disabledWorksheet) })],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 1, startColumn: 1 } }] }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[ISheetsHyperLinkPopupService, { currentEditing: null }],
|
||||
]))).toBe(false);
|
||||
|
||||
expect(InsertHyperLinkToolbarOperation.handler(createAccessor([
|
||||
[IUniverInstanceService, createUniverInstanceService({ workbook: createWorkbook(enabledWorksheet) })],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 1, startColumn: 1 } }] }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[ISheetsHyperLinkPopupService, { currentEditing: { row: 1 } }],
|
||||
]))).toBe(true);
|
||||
expect(InsertHyperLinkToolbarOperation.handler(createAccessor([
|
||||
[IUniverInstanceService, createUniverInstanceService({ workbook: createWorkbook(enabledWorksheet) })],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 1, startColumn: 1 } }] }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[ISheetsHyperLinkPopupService, { currentEditing: null }],
|
||||
]))).toBe(true);
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(CloseHyperLinkPopupOperation.id);
|
||||
expect(executeCommand).toHaveBeenCalledWith(InsertHyperLinkOperation.id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { ISheetCommandSharedParams } from '@univerjs/sheets';
|
||||
import { CommandType, ICommandService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { ISheetsHyperLinkPopupService } from '../../services/popup.service';
|
||||
import { HyperLinkEditSourceType } from '../../types/enums/edit-source';
|
||||
import { getShouldDisableCurrentCellLink } from '../../utils';
|
||||
|
||||
export interface IOpenHyperLinkEditPanelOperationParams extends ISheetCommandSharedParams {
|
||||
row: number;
|
||||
col: number;
|
||||
customRangeId?: string;
|
||||
type: HyperLinkEditSourceType;
|
||||
}
|
||||
|
||||
export const OpenHyperLinkEditPanelOperation: ICommand<IOpenHyperLinkEditPanelOperationParams> = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'sheet.operation.open-hyper-link-edit-panel',
|
||||
handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const popupService = accessor.get(ISheetsHyperLinkPopupService);
|
||||
if (!params.customRangeId) {
|
||||
popupService.startAddEditing(params);
|
||||
} else {
|
||||
popupService.startEditing(params as Required<IOpenHyperLinkEditPanelOperationParams>);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseHyperLinkPopupOperation: ICommand = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'sheet.operation.close-hyper-link-popup',
|
||||
handler(accessor) {
|
||||
const popupService = accessor.get(ISheetsHyperLinkPopupService);
|
||||
|
||||
popupService.endEditing();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertHyperLinkOperation: ICommand = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'sheet.operation.insert-hyper-link',
|
||||
handler(accessor) {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const selection = selectionManagerService.getCurrentLastSelection();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const row = selection.range.startRow;
|
||||
const col = selection.range.startColumn;
|
||||
const visible = editorBridgeService.isVisible();
|
||||
return commandService.executeCommand(OpenHyperLinkEditPanelOperation.id, {
|
||||
unitId: target.unitId,
|
||||
subUnitId: target.subUnitId,
|
||||
row,
|
||||
col,
|
||||
type: visible.visible ? HyperLinkEditSourceType.EDITING : HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertHyperLinkToolbarOperation: ICommand = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'sheet.operation.insert-hyper-link-toolbar',
|
||||
handler(accessor) {
|
||||
if (getShouldDisableCurrentCellLink(accessor)) {
|
||||
return false;
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const popupService = accessor.get(ISheetsHyperLinkPopupService);
|
||||
if (popupService.currentEditing) {
|
||||
return commandService.executeCommand(CloseHyperLinkPopupOperation.id);
|
||||
} else {
|
||||
return commandService.executeCommand(InsertHyperLinkOperation.id);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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 { describe, expect, it } from 'vitest';
|
||||
import { hasProtocol, isEmail, serializeUrl } from '../util';
|
||||
|
||||
describe('hyper-link common util', () => {
|
||||
it('should detect protocols and email addresses', () => {
|
||||
expect(hasProtocol('https://univer.ai')).toBe(true);
|
||||
expect(hasProtocol('univer.ai')).toBe(false);
|
||||
expect(isEmail('test@example.com')).toBe(true);
|
||||
expect(isEmail('not-an-email')).toBe(false);
|
||||
});
|
||||
|
||||
it('should normalize external URLs and keep same-page anchors compact', () => {
|
||||
Object.defineProperty(globalThis, 'location', {
|
||||
configurable: true,
|
||||
value: {
|
||||
hostname: 'localhost',
|
||||
port: '',
|
||||
protocol: 'http:',
|
||||
pathname: '/sheet',
|
||||
},
|
||||
});
|
||||
|
||||
expect(serializeUrl('https://univer.ai')).toBe('https://univer.ai');
|
||||
expect(serializeUrl('not a legal link')).toBe('not a legal link');
|
||||
});
|
||||
});
|
||||
59
packages/sheets-hyper-link-ui/src/common/util.ts
Normal file
59
packages/sheets-hyper-link-ui/src/common/util.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 { Tools } from '@univerjs/core';
|
||||
|
||||
export function isLegalLink(link: string) {
|
||||
return Tools.isLegalUrl(link);
|
||||
}
|
||||
|
||||
export function hasProtocol(urlString: string) {
|
||||
const pattern = /^[a-zA-Z]+:\/\//;
|
||||
return pattern.test(urlString);
|
||||
}
|
||||
|
||||
export function isEmail(url: string) {
|
||||
const pattern = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
|
||||
return pattern.test(url);
|
||||
}
|
||||
|
||||
export function serializeUrl(urlStr: string) {
|
||||
if (isLegalLink(urlStr)) {
|
||||
const transformedUrl = hasProtocol(urlStr) ? urlStr : isEmail(urlStr) ? `mailto://${urlStr}` : `http://${urlStr}`;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(transformedUrl);
|
||||
} catch {
|
||||
return urlStr;
|
||||
}
|
||||
|
||||
if (
|
||||
url.hostname === location.hostname &&
|
||||
url.port === location.port &&
|
||||
url.protocol === location.protocol &&
|
||||
url.pathname === location.pathname &&
|
||||
url.hash &&
|
||||
!url.search
|
||||
) {
|
||||
return url.hash;
|
||||
}
|
||||
|
||||
return transformedUrl;
|
||||
}
|
||||
|
||||
return urlStr;
|
||||
}
|
||||
31
packages/sheets-hyper-link-ui/src/config/config.ts
Normal file
31
packages/sheets-hyper-link-ui/src/config/config.ts
Normal 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 { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY = 'sheets-hyper-link-ui.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUrlHandler {
|
||||
navigateToOtherWebsite?: (url: string) => void;
|
||||
}
|
||||
export interface IUniverSheetsHyperLinkUIConfig {
|
||||
menu?: MenuConfig;
|
||||
urlHandler?: IUrlHandler;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsHyperLinkUIConfig = {};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 { AUTO_FILL_APPLY_TYPE } from '@univerjs/sheets';
|
||||
import { AddHyperLinkMutation, RemoveHyperLinkMutation } from '@univerjs/sheets-hyper-link';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsHyperLinkAutoFillController } from '../auto-fill.controller';
|
||||
|
||||
describe('SheetsHyperLinkAutoFillController', () => {
|
||||
it('should generate add/remove link mutations when copying', () => {
|
||||
let capturedHook: any;
|
||||
const addHook = vi.fn((hook) => {
|
||||
capturedHook = hook;
|
||||
return { dispose: vi.fn() };
|
||||
});
|
||||
|
||||
const autoFillService = {
|
||||
addHook,
|
||||
} as any;
|
||||
|
||||
const hyperLinkModel = {
|
||||
getHyperLinkByLocation: vi.fn((unitId: string, subUnitId: string, row: number, col: number) => {
|
||||
if (unitId !== 'u1' || subUnitId !== 's1') return null;
|
||||
if (row === 0 && col === 0) {
|
||||
return { id: 'link-1', payload: '#gid=sheet&range=A1', row, column: col };
|
||||
}
|
||||
if (row === 1 && col === 0) {
|
||||
return { id: 'current-link', payload: '#gid=sheet&range=A2', row, column: col };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
// init registers hook
|
||||
const controller = new SheetsHyperLinkAutoFillController(autoFillService, hyperLinkModel);
|
||||
expect(addHook).toHaveBeenCalledTimes(1);
|
||||
|
||||
const res = capturedHook.onFillData({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
source: { rows: [0], cols: [0] },
|
||||
// target should be the filled destination range, not including source
|
||||
target: { rows: [1], cols: [0] },
|
||||
}, 'DOWN', AUTO_FILL_APPLY_TYPE.COPY);
|
||||
|
||||
expect(res.redos.map((m: any) => m.id)).toEqual([RemoveHyperLinkMutation.id, AddHyperLinkMutation.id]);
|
||||
expect(res.undos.map((m: any) => m.id)).toEqual([RemoveHyperLinkMutation.id, AddHyperLinkMutation.id]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should return noop mutations for non-copy apply type', () => {
|
||||
let capturedHook: any;
|
||||
const autoFillService = {
|
||||
addHook: vi.fn((hook) => {
|
||||
capturedHook = hook;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const hyperLinkModel = {
|
||||
getHyperLinkByLocation: vi.fn(() => null),
|
||||
} as any;
|
||||
|
||||
const controller = new SheetsHyperLinkAutoFillController(autoFillService, hyperLinkModel);
|
||||
|
||||
const res = capturedHook.onFillData({
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
source: { rows: [0], cols: [0] },
|
||||
target: { rows: [0, 1], cols: [0] },
|
||||
}, 'DOWN', AUTO_FILL_APPLY_TYPE.NO_FORMAT);
|
||||
|
||||
expect(res).toEqual({ redos: [], undos: [] });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { AddHyperLinkMutation, RemoveHyperLinkMutation } from '@univerjs/sheets-hyper-link';
|
||||
import { COPY_TYPE, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsHyperLinkCopyPasteController } from '../copy-paste.controller';
|
||||
|
||||
describe('SheetsHyperLinkCopyPasteController', () => {
|
||||
it('should generate remove/add mutations when pasting a copied hyperlink cell', () => {
|
||||
let hook: any;
|
||||
const addClipboardHook = vi.fn((h) => {
|
||||
hook = h;
|
||||
});
|
||||
|
||||
const sheetClipboardService = {
|
||||
addClipboardHook,
|
||||
} as any;
|
||||
|
||||
const hyperLinkModel = {
|
||||
getHyperLinkByLocation: vi.fn((unitId: string, subUnitId: string, row: number, col: number) => {
|
||||
if (unitId === 'u1' && subUnitId === 's1' && row === 0 && col === 0) {
|
||||
return { id: 'old-id', payload: '#gid=s1&range=A1', row, column: col };
|
||||
}
|
||||
|
||||
if (unitId === 'u2' && subUnitId === 's2' && row === 1 && col === 1) {
|
||||
return { id: 'current-id', payload: '#gid=s2&range=B2', row, column: col };
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
getHyperLink: vi.fn((unitId: string, subUnitId: string, id: string) => {
|
||||
if (unitId === 'u1' && subUnitId === 's1' && id === 'old-id') {
|
||||
return { id, payload: '#gid=s1&range=A1', row: 0, column: 0 };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const injector = {
|
||||
invoke: vi.fn(() => ({ rows: [0], cols: [0] })),
|
||||
} as any;
|
||||
|
||||
const resolverService = {} as any;
|
||||
|
||||
const controller = new SheetsHyperLinkCopyPasteController(sheetClipboardService, hyperLinkModel, injector, resolverService);
|
||||
expect(addClipboardHook).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simulate copy stage
|
||||
hook.onBeforeCopy('u1', 's1', { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 });
|
||||
|
||||
const res = hook.onPasteCells(
|
||||
{ range: { rows: [0], cols: [0] } },
|
||||
{ unitId: 'u2', subUnitId: 's2', range: { rows: [1], cols: [1] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE, copyRange: { rows: [0], cols: [0] } }
|
||||
);
|
||||
|
||||
expect(res.redos.map((m: any) => m.id)).toEqual([RemoveHyperLinkMutation.id, AddHyperLinkMutation.id]);
|
||||
expect(res.undos.map((m: any) => m.id)).toEqual([RemoveHyperLinkMutation.id, AddHyperLinkMutation.id]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should ignore special paste types', () => {
|
||||
let hook: any;
|
||||
const sheetClipboardService = {
|
||||
addClipboardHook: vi.fn((h) => {
|
||||
hook = h;
|
||||
}),
|
||||
} as any;
|
||||
const hyperLinkModel = { getHyperLinkByLocation: vi.fn(), getHyperLink: vi.fn() } as any;
|
||||
const injector = { invoke: vi.fn(() => ({ rows: [0], cols: [0] })) } as any;
|
||||
const resolverService = {} as any;
|
||||
|
||||
const controller = new SheetsHyperLinkCopyPasteController(sheetClipboardService, hyperLinkModel, injector, resolverService);
|
||||
hook.onBeforeCopy('u1', 's1', { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 });
|
||||
|
||||
const res = hook.onPasteCells(
|
||||
{ range: { rows: [0], cols: [0] } },
|
||||
{ unitId: 'u2', subUnitId: 's2', range: { rows: [1], cols: [1] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE, copyRange: { rows: [0], cols: [0] } }
|
||||
);
|
||||
|
||||
expect(res).toEqual({ redos: [], undos: [] });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -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 { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { InsertLinkShortcut } from '../../menu/menu';
|
||||
import { SheetsHyperLinkPermissionController } from '../hyper-link-permission.controller';
|
||||
|
||||
describe('SheetsHyperLinkPermissionController', () => {
|
||||
let beforeCommandExecuted: any;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeCommandExecuted = undefined;
|
||||
});
|
||||
|
||||
it('should block shortcut execution when permission fails', () => {
|
||||
const localeService = { t: (key: string) => key } as any;
|
||||
|
||||
const commandService = {
|
||||
beforeCommandExecuted: vi.fn((fn) => {
|
||||
beforeCommandExecuted = fn;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const blockExecuteWithoutPermission = vi.fn();
|
||||
const sheetPermissionCheckController = {
|
||||
permissionCheckWithRanges: vi.fn(() => false),
|
||||
blockExecuteWithoutPermission,
|
||||
} as any;
|
||||
|
||||
const controller = new SheetsHyperLinkPermissionController(localeService, commandService, sheetPermissionCheckController);
|
||||
|
||||
beforeCommandExecuted({ id: InsertLinkShortcut.id });
|
||||
expect(blockExecuteWithoutPermission).toHaveBeenCalledWith('sheets-hyper-link-ui.permission.hyperLinkErr');
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('should not block when permission passes', () => {
|
||||
const localeService = { t: (key: string) => key } as any;
|
||||
|
||||
const commandService = {
|
||||
beforeCommandExecuted: vi.fn((fn) => {
|
||||
beforeCommandExecuted = fn;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const blockExecuteWithoutPermission = vi.fn();
|
||||
const sheetPermissionCheckController = {
|
||||
permissionCheckWithRanges: vi.fn(() => true),
|
||||
blockExecuteWithoutPermission,
|
||||
} as any;
|
||||
|
||||
const controller = new SheetsHyperLinkPermissionController(localeService, commandService, sheetPermissionCheckController);
|
||||
beforeCommandExecuted({ id: InsertLinkShortcut.id });
|
||||
expect(blockExecuteWithoutPermission).not.toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { awaitTime } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsHyperLinkRenderController } from '../render-controllers/render.controller';
|
||||
|
||||
describe('SheetsHyperLinkRenderController', () => {
|
||||
it('should mark skeleton dirty when link updates', async () => {
|
||||
const makeForceDirty = vi.fn();
|
||||
const linkUpdate$ = new Subject<void>();
|
||||
|
||||
const controller = new SheetsHyperLinkRenderController({
|
||||
mainComponent: { makeForceDirty },
|
||||
} as any, { linkUpdate$ } as any);
|
||||
|
||||
linkUpdate$.next();
|
||||
|
||||
// debounced by 16ms
|
||||
await awaitTime(30);
|
||||
expect(makeForceDirty).toHaveBeenCalledTimes(1);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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 { IMutationInfo } from '@univerjs/core';
|
||||
import type { IAutoFillLocation, ISheetAutoFillHook } from '@univerjs/sheets';
|
||||
import { Disposable, generateRandomId, Inject, Range, Rectangle } from '@univerjs/core';
|
||||
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
|
||||
import { AddHyperLinkMutation, HyperLinkModel, RemoveHyperLinkMutation } from '@univerjs/sheets-hyper-link';
|
||||
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
|
||||
import { SHEET_HYPER_LINK_UI_PLUGIN } from '../types/const';
|
||||
|
||||
export class SheetsHyperLinkAutoFillController extends Disposable {
|
||||
constructor(
|
||||
@IAutoFillService private readonly _autoFillService: IAutoFillService,
|
||||
@Inject(HyperLinkModel) private readonly _hyperLinkModel: HyperLinkModel
|
||||
) {
|
||||
super();
|
||||
this._initAutoFill();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initAutoFill() {
|
||||
const noopReturnFunc = () => ({ redos: [], undos: [] });
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
const generalApplyFunc = (location: IAutoFillLocation, applyType: AUTO_FILL_APPLY_TYPE) => {
|
||||
const { source: sourceRange, target: targetRange, unitId, subUnitId } = location;
|
||||
|
||||
const virtualRange = virtualizeDiscreteRanges([sourceRange, targetRange]);
|
||||
const [vSourceRange, vTargetRange] = virtualRange.ranges;
|
||||
const { mapFunc } = virtualRange;
|
||||
const sourceStartCell = {
|
||||
row: vSourceRange.startRow,
|
||||
col: vSourceRange.startColumn,
|
||||
};
|
||||
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
repeats.forEach((repeat) => {
|
||||
const targetStartCell = repeat.repeatStartCell;
|
||||
const relativeRange = repeat.relativeRange;
|
||||
const sourceRange = {
|
||||
startRow: sourceStartCell.row,
|
||||
startColumn: sourceStartCell.col,
|
||||
endColumn: sourceStartCell.col,
|
||||
endRow: sourceStartCell.row,
|
||||
};
|
||||
const targetRange = {
|
||||
startRow: targetStartCell.row,
|
||||
startColumn: targetStartCell.col,
|
||||
endColumn: targetStartCell.col,
|
||||
endRow: targetStartCell.row,
|
||||
};
|
||||
Range.foreach(relativeRange, (row, col) => {
|
||||
const sourcePositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
sourceRange
|
||||
);
|
||||
const { row: sourceRow, col: sourceCol } = mapFunc(sourcePositionRange.startRow, sourcePositionRange.startColumn);
|
||||
const link = this._hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, sourceRow, sourceCol);
|
||||
const targetPositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
targetRange
|
||||
);
|
||||
const { row: targetRow, col: targetCol } = mapFunc(targetPositionRange.startRow, targetPositionRange.startColumn);
|
||||
const id = generateRandomId();
|
||||
const currentLink = this._hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, targetRow, targetCol);
|
||||
if (currentLink) {
|
||||
redos.push({
|
||||
id: RemoveHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id: currentLink.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
if ((AUTO_FILL_APPLY_TYPE.COPY === applyType || AUTO_FILL_APPLY_TYPE.SERIES === applyType) && link) {
|
||||
redos.push({
|
||||
id: AddHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
link: {
|
||||
...link,
|
||||
id,
|
||||
row: targetRow,
|
||||
column: targetCol,
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: RemoveHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (currentLink) {
|
||||
undos.push({
|
||||
id: AddHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
link: currentLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
undos,
|
||||
redos,
|
||||
};
|
||||
};
|
||||
const hook: ISheetAutoFillHook = {
|
||||
id: SHEET_HYPER_LINK_UI_PLUGIN,
|
||||
onFillData: (location, direction, applyType) => {
|
||||
if (
|
||||
applyType === AUTO_FILL_APPLY_TYPE.COPY ||
|
||||
applyType === AUTO_FILL_APPLY_TYPE.ONLY_FORMAT ||
|
||||
applyType === AUTO_FILL_APPLY_TYPE.SERIES
|
||||
) {
|
||||
return generalApplyFunc(location, applyType);
|
||||
}
|
||||
|
||||
return noopReturnFunc();
|
||||
},
|
||||
};
|
||||
this.disposeWithMe(this._autoFillService.addHook(hook));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 { LinkIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { CellLinkEdit } from '../views/CellLinkEdit';
|
||||
import { CellLinkPopup } from '../views/CellLinkPopup';
|
||||
|
||||
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 {
|
||||
([
|
||||
[CellLinkPopup.componentKey, CellLinkPopup],
|
||||
[CellLinkEdit.componentKey, CellLinkEdit],
|
||||
] as const).forEach(([key, comp]) => {
|
||||
this.disposeWithMe(this._componentManager.register(key, comp));
|
||||
});
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
LinkIcon,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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 { IMutationInfo, IRange, Nullable } from '@univerjs/core';
|
||||
import type { IDiscreteRange } from '@univerjs/sheets';
|
||||
import type { IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
|
||||
import { Disposable, generateRandomId, Inject, Injector, ObjectMatrix, Range, Rectangle } from '@univerjs/core';
|
||||
import { rangeToDiscreteRange } from '@univerjs/sheets';
|
||||
import { AddHyperLinkMutation, HyperLinkModel, RemoveHyperLinkMutation } from '@univerjs/sheets-hyper-link';
|
||||
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
|
||||
import { isLegalLink } from '../common/util';
|
||||
import { SheetsHyperLinkResolverService } from '../services/resolver.service';
|
||||
import { SHEET_HYPER_LINK_UI_PLUGIN } from '../types/const';
|
||||
|
||||
export class SheetsHyperLinkCopyPasteController extends Disposable {
|
||||
private _plainTextFilter = new Set<(text: string) => boolean>();
|
||||
|
||||
registerPlainTextFilter(filter: (text: string) => boolean) {
|
||||
this._plainTextFilter.add(filter);
|
||||
}
|
||||
|
||||
removePlainTextFilter(filter: (text: string) => boolean) {
|
||||
this._plainTextFilter.delete(filter);
|
||||
}
|
||||
|
||||
/* If return false the process of paste text will be stop */
|
||||
private _filterPlainText(text: string) {
|
||||
return Array.from(this._plainTextFilter).every((filter) => filter(text));
|
||||
}
|
||||
|
||||
private _copyInfo: Nullable<{
|
||||
matrix: ObjectMatrix<string>;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
@ISheetClipboardService private _sheetClipboardService: ISheetClipboardService,
|
||||
@Inject(HyperLinkModel) private _hyperLinkModel: HyperLinkModel,
|
||||
@Inject(Injector) private _injector: Injector,
|
||||
@Inject(SheetsHyperLinkResolverService) private _resolverService: SheetsHyperLinkResolverService
|
||||
) {
|
||||
super();
|
||||
this._initCopyPaste();
|
||||
this.disposeWithMe(() => {
|
||||
this._plainTextFilter.clear();
|
||||
});
|
||||
}
|
||||
|
||||
private _initCopyPaste() {
|
||||
this._sheetClipboardService.addClipboardHook({
|
||||
id: SHEET_HYPER_LINK_UI_PLUGIN,
|
||||
onBeforeCopy: (unitId, subUnitId, range) => this._collect(unitId, subUnitId, range),
|
||||
onPasteCells: (pasteFrom, pasteTo, data, payload) => {
|
||||
const { copyType = COPY_TYPE.COPY, pasteType } = payload;
|
||||
const { range: copyRange } = pasteFrom || {};
|
||||
const { range: pastedRange, unitId, subUnitId } = pasteTo;
|
||||
return this._generateMutations(pastedRange, { copyType, pasteType, copyRange, unitId, subUnitId });
|
||||
},
|
||||
onPastePlainText: (pasteTo: ISheetDiscreteRangeLocation, clipText: string) => {
|
||||
const filterResult = this._filterPlainText(clipText);
|
||||
|
||||
if (isLegalLink(clipText) && filterResult) {
|
||||
const { range, unitId, subUnitId } = pasteTo;
|
||||
const { ranges: [pasteToRange], mapFunc } = virtualizeDiscreteRanges([range]);
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
Range.foreach(pasteToRange, (originRow, originCol) => {
|
||||
const { row, col: column } = mapFunc(originRow, originCol);
|
||||
const link = this._hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, column);
|
||||
if (link) {
|
||||
redos.push({
|
||||
id: RemoveHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id: link.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (link) {
|
||||
undos.push({
|
||||
id: AddHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
link,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
return { undos: [], redos: [] };
|
||||
},
|
||||
priority: 99,
|
||||
});
|
||||
}
|
||||
|
||||
private _collect(unitId: string, subUnitId: string, range: IRange) {
|
||||
const matrix = new ObjectMatrix<string>();
|
||||
this._copyInfo = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
matrix,
|
||||
};
|
||||
|
||||
const discreteRange = this._injector.invoke((accessor) => {
|
||||
return rangeToDiscreteRange(range, accessor, unitId, subUnitId);
|
||||
});
|
||||
if (!discreteRange) {
|
||||
return;
|
||||
}
|
||||
const { rows, cols } = discreteRange;
|
||||
rows.forEach((row, rowIndex) => {
|
||||
cols.forEach((col, colIndex) => {
|
||||
const link = this._hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, col);
|
||||
|
||||
matrix.setValue(rowIndex, colIndex, link?.id ?? '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _generateMutations(
|
||||
pastedRange: IDiscreteRange,
|
||||
copyInfo: {
|
||||
copyType: COPY_TYPE;
|
||||
copyRange?: IDiscreteRange;
|
||||
pasteType: IPasteHookValueType;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
) {
|
||||
if (!this._copyInfo) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
if (!this._copyInfo || !this._copyInfo.matrix.getSizeOf() || !copyInfo.copyRange) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const specialPastes: IPasteHookValueType[] = [
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
];
|
||||
|
||||
if (specialPastes.includes(copyInfo.pasteType)) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId } = this._copyInfo;
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyInfo.copyRange, pastedRange]);
|
||||
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
|
||||
|
||||
repeatRange.forEach(({ startRange }) => {
|
||||
this._copyInfo?.matrix.forValue((row, col, ruleId) => {
|
||||
const range = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
},
|
||||
startRange
|
||||
);
|
||||
const oldLink = this._hyperLinkModel.getHyperLink(unitId, subUnitId, ruleId);
|
||||
|
||||
const { row: startRow, col: startColumn } = mapFunc(range.startRow, range.startColumn);
|
||||
const currentLink = this._hyperLinkModel.getHyperLinkByLocation(copyInfo.unitId, copyInfo.subUnitId, startRow, startColumn);
|
||||
const id = generateRandomId();
|
||||
if (currentLink) {
|
||||
redos.push({
|
||||
id: RemoveHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId: copyInfo.unitId,
|
||||
subUnitId: copyInfo.subUnitId,
|
||||
id: currentLink.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (oldLink) {
|
||||
redos.push({
|
||||
id: AddHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId: copyInfo.unitId,
|
||||
subUnitId: copyInfo.subUnitId,
|
||||
link: {
|
||||
...oldLink,
|
||||
id,
|
||||
row: startRow,
|
||||
column: startColumn,
|
||||
},
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: RemoveHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId: copyInfo.unitId,
|
||||
subUnitId: copyInfo.subUnitId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (currentLink) {
|
||||
undos.push({
|
||||
id: AddHyperLinkMutation.id,
|
||||
params: {
|
||||
unitId: copyInfo.unitId,
|
||||
subUnitId: copyInfo.subUnitId,
|
||||
link: currentLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 { ICommandInfo } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { Disposable, ICommandService, Inject, LocaleService } from '@univerjs/core';
|
||||
import {
|
||||
RangeProtectionPermissionEditPoint,
|
||||
SheetPermissionCheckController,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetInsertHyperlinkPermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { InsertLinkShortcut } from '../menu/menu';
|
||||
|
||||
export class SheetsHyperLinkPermissionController extends Disposable {
|
||||
constructor(
|
||||
@Inject(LocaleService) private _localeService: LocaleService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController
|
||||
) {
|
||||
super();
|
||||
|
||||
this._commandExecutedListener();
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === InsertLinkShortcut.id) {
|
||||
const permission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellValuePermission, WorksheetInsertHyperlinkPermission],
|
||||
});
|
||||
if (!permission) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(
|
||||
this._localeService.t<LocaleKey>('sheets-hyper-link-ui.permission.hyperLinkErr')
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 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 { ICellData } from '@univerjs/core';
|
||||
import type { IHoverRichTextInfo } from '@univerjs/sheets-ui';
|
||||
import type { IHyperLinkPopupOptions } from '../../../services/popup.service';
|
||||
import {
|
||||
CustomRangeType,
|
||||
ICommandService,
|
||||
Injector,
|
||||
IPermissionService,
|
||||
IUniverInstanceService,
|
||||
RichTextBuilder,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetPermissionCheckController } from '@univerjs/sheets';
|
||||
import { HoverManagerService, IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ISheetsHyperLinkPopupService } from '../../../services/popup.service';
|
||||
import { SheetsHyperLinkMobilePopupController } from '../popup.controller';
|
||||
|
||||
class TestHoverManagerService {
|
||||
readonly currentClickedCell$ = new Subject<IHoverRichTextInfo>();
|
||||
}
|
||||
|
||||
class TestSheetsHyperLinkPopupService {
|
||||
readonly shown: IHyperLinkPopupOptions[] = [];
|
||||
hideCount = 0;
|
||||
|
||||
showPopup(location: IHyperLinkPopupOptions): void {
|
||||
this.shown.push(location);
|
||||
}
|
||||
|
||||
hideCurrentPopup(): void {
|
||||
this.hideCount += 1;
|
||||
}
|
||||
|
||||
endEditing(): void {}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
getRenderUnitById() {
|
||||
return {
|
||||
with: () => ({
|
||||
getSkeletonParam: () => ({
|
||||
skeleton: {
|
||||
overflowCache: {
|
||||
forValue: () => {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestPermissionService {
|
||||
composePermission() {
|
||||
return [{ value: true }];
|
||||
}
|
||||
}
|
||||
|
||||
class TestSheetPermissionCheckController {
|
||||
permissionCheckWithRanges(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandService {
|
||||
onCommandExecuted() {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
class TestEditorBridgeService {
|
||||
readonly currentEditCellState$ = of(null);
|
||||
readonly visible$ = of({ visible: false });
|
||||
}
|
||||
|
||||
class TestDocSelectionManagerService {
|
||||
readonly textSelection$ = new Subject();
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
cell: ICellData = {};
|
||||
|
||||
readonly worksheet = {
|
||||
getCell: () => this.cell,
|
||||
getCellRaw: () => ({}),
|
||||
getCellStyleOnly: () => ({}),
|
||||
};
|
||||
|
||||
readonly workbook = {
|
||||
getSheetBySheetId: () => this.worksheet,
|
||||
getStyles: () => ({
|
||||
getStyleByCell: () => ({}),
|
||||
}),
|
||||
};
|
||||
|
||||
getUnit() {
|
||||
return this.workbook;
|
||||
}
|
||||
}
|
||||
|
||||
function createMobilePopupControllerTestBed() {
|
||||
const injector = new Injector();
|
||||
injector.add([HoverManagerService, { useClass: TestHoverManagerService as never }]);
|
||||
injector.add([ISheetsHyperLinkPopupService, { useClass: TestSheetsHyperLinkPopupService as never }]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([IPermissionService, { useClass: TestPermissionService as never }]);
|
||||
injector.add([SheetPermissionCheckController, { useClass: TestSheetPermissionCheckController as never }]);
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
injector.add([IEditorBridgeService, { useClass: TestEditorBridgeService as never }]);
|
||||
injector.add([DocSelectionManagerService, { useClass: TestDocSelectionManagerService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([SheetsHyperLinkMobilePopupController]);
|
||||
injector.get(SheetsHyperLinkMobilePopupController);
|
||||
|
||||
return {
|
||||
hoverManagerService: injector.get(HoverManagerService) as unknown as TestHoverManagerService,
|
||||
popupService: injector.get(ISheetsHyperLinkPopupService) as unknown as TestSheetsHyperLinkPopupService,
|
||||
univerInstanceService: injector.get(IUniverInstanceService) as unknown as TestUniverInstanceService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SheetsHyperLinkMobilePopupController', () => {
|
||||
it('opens the hyperlink popup when the hyperlink text is clicked', () => {
|
||||
const { hoverManagerService, popupService } = createMobilePopupControllerTestBed();
|
||||
const customRange = {
|
||||
rangeId: 'link-1',
|
||||
rangeType: CustomRangeType.HYPERLINK,
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
properties: { url: 'https://univer.ai' },
|
||||
};
|
||||
|
||||
hoverManagerService.currentClickedCell$.next({
|
||||
location: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
},
|
||||
position: { startX: 0, startY: 0, endX: 20, endY: 20 },
|
||||
customRange,
|
||||
rect: { left: 0, top: 0, right: 20, bottom: 20 },
|
||||
});
|
||||
|
||||
expect(popupService.shown).toEqual([
|
||||
expect.objectContaining({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
customRange,
|
||||
editPermission: true,
|
||||
copyPermission: true,
|
||||
}),
|
||||
]);
|
||||
expect(popupService.hideCount).toBe(0);
|
||||
});
|
||||
|
||||
it('opens the hyperlink popup when the empty area of a hyperlink cell is clicked', () => {
|
||||
const { hoverManagerService, popupService, univerInstanceService } = createMobilePopupControllerTestBed();
|
||||
const documentData = RichTextBuilder.create()
|
||||
.insertLink('Univer', 'https://univer.ai')
|
||||
.getData();
|
||||
const customRange = documentData.body!.customRanges![0];
|
||||
univerInstanceService.cell = {
|
||||
p: documentData,
|
||||
};
|
||||
|
||||
hoverManagerService.currentClickedCell$.next({
|
||||
location: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
},
|
||||
position: { startX: 0, startY: 0, endX: 100, endY: 20 },
|
||||
});
|
||||
|
||||
expect(popupService.shown).toEqual([
|
||||
expect.objectContaining({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
customRange,
|
||||
editPermission: true,
|
||||
copyPermission: true,
|
||||
}),
|
||||
]);
|
||||
expect(popupService.hideCount).toBe(0);
|
||||
});
|
||||
|
||||
it('shows all hyperlinks when the empty area of a cell containing multiple hyperlinks is clicked', () => {
|
||||
const { hoverManagerService, popupService, univerInstanceService } = createMobilePopupControllerTestBed();
|
||||
const documentData = RichTextBuilder.create()
|
||||
.insertLink('Univer', 'https://univer.ai')
|
||||
.insertText(' ')
|
||||
.insertLink('Docs', 'https://docs.univer.ai')
|
||||
.getData();
|
||||
const firstCustomRange = documentData.body!.customRanges![0];
|
||||
univerInstanceService.cell = {
|
||||
p: documentData,
|
||||
};
|
||||
|
||||
hoverManagerService.currentClickedCell$.next({
|
||||
location: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
},
|
||||
position: { startX: 0, startY: 0, endX: 100, endY: 20 },
|
||||
});
|
||||
|
||||
expect(popupService.shown).toEqual([
|
||||
expect.objectContaining({
|
||||
customRange: firstCustomRange,
|
||||
showAll: true,
|
||||
}),
|
||||
]);
|
||||
expect(popupService.hideCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { LinkIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { MobileCellLinkEdit } from '../../views/MobileCellLinkEdit';
|
||||
import { MobileCellLinkPopup } from '../../views/MobileCellLinkPopup';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) componentManager: ComponentManager,
|
||||
@Inject(IconManager) iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(componentManager.register(MobileCellLinkPopup.componentKey, MobileCellLinkPopup));
|
||||
this.disposeWithMe(componentManager.register(MobileCellLinkEdit.componentKey, MobileCellLinkEdit));
|
||||
this.disposeWithMe(iconManager.register({ LinkIcon }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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 { ICustomRange, Nullable, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { ISheetLocationBase } from '@univerjs/sheets';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import {
|
||||
CustomRangeType,
|
||||
Disposable,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IPermissionService,
|
||||
IUniverInstanceService,
|
||||
Rectangle,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DocEventManagerService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
ClearSelectionAllCommand,
|
||||
ClearSelectionContentCommand,
|
||||
ClearSelectionFormatCommand,
|
||||
RangeProtectionPermissionEditPoint,
|
||||
RangeProtectionPermissionViewPoint,
|
||||
SheetPermissionCheckController,
|
||||
WorkbookCopyPermission,
|
||||
WorkbookEditablePermission,
|
||||
WorkbookViewPermission,
|
||||
WorksheetCopyPermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetInsertHyperlinkPermission,
|
||||
WorksheetViewPermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { HoverManagerService, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { map, switchMap } from 'rxjs';
|
||||
import { ISheetsHyperLinkPopupService } from '../../services/popup.service';
|
||||
import { HyperLinkEditSourceType } from '../../types/enums/edit-source';
|
||||
|
||||
export class SheetsHyperLinkMobilePopupController extends Disposable {
|
||||
constructor(
|
||||
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
|
||||
@Inject(ISheetsHyperLinkPopupService) private readonly _sheetsHyperLinkPopupService: ISheetsHyperLinkPopupService,
|
||||
@Inject(IRenderManagerService) private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(IPermissionService) private readonly _permissionService: IPermissionService,
|
||||
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initClickListener();
|
||||
this._initCommandListener();
|
||||
this._initClickEditingListener();
|
||||
this._initTextSelectionListener();
|
||||
}
|
||||
|
||||
private _getLinkPermission(location: ISheetLocationBase) {
|
||||
const { unitId, subUnitId, row, col } = location;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) {
|
||||
return {
|
||||
viewPermission: false,
|
||||
editPermission: false,
|
||||
copyPermission: false,
|
||||
};
|
||||
}
|
||||
|
||||
const range = [{ startRow: row, startColumn: col, endRow: row, endColumn: col }];
|
||||
const viewPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookViewPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}, range, unitId, subUnitId);
|
||||
let editPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetInsertHyperlinkPermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}, range, unitId, subUnitId);
|
||||
|
||||
const cell = worksheet.getCellRaw(row, col);
|
||||
if (cell?.f?.startsWith('=HYPERLINK(')) {
|
||||
editPermission = false;
|
||||
}
|
||||
|
||||
const copyPermission = this._permissionService.composePermission([
|
||||
new WorkbookCopyPermission(unitId).id,
|
||||
new WorksheetCopyPermission(unitId, subUnitId).id,
|
||||
]).every((permission) => permission.value);
|
||||
|
||||
return {
|
||||
viewPermission,
|
||||
editPermission,
|
||||
copyPermission,
|
||||
};
|
||||
}
|
||||
|
||||
private _getCellHyperLinkInfo(
|
||||
worksheet: Worksheet,
|
||||
row: number,
|
||||
col: number,
|
||||
clickedCustomRange?: Nullable<ICustomRange>
|
||||
) {
|
||||
const customRanges = worksheet.getCell(row, col)?.p?.body?.customRanges ?? [];
|
||||
const cellHyperLinks = customRanges.filter(
|
||||
(customRange) => customRange.rangeType === CustomRangeType.HYPERLINK
|
||||
);
|
||||
const clickedHyperLink = clickedCustomRange?.rangeType === CustomRangeType.HYPERLINK
|
||||
? clickedCustomRange
|
||||
: null;
|
||||
|
||||
return {
|
||||
cellHyperLinks,
|
||||
clickedHyperLink,
|
||||
customRange: clickedHyperLink ?? cellHyperLinks[0],
|
||||
};
|
||||
}
|
||||
|
||||
private _initClickListener(): void {
|
||||
this.disposeWithMe(this._hoverManagerService.currentClickedCell$.subscribe((currentCell) => {
|
||||
if (!currentCell) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, row, col } = currentCell.location;
|
||||
const renderer = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!renderer || !workbook || !worksheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = renderer.with(SheetSkeletonManagerService)
|
||||
.getSkeletonParam(subUnitId)
|
||||
?.skeleton;
|
||||
let targetRow = row;
|
||||
let targetCol = col;
|
||||
if (skeleton) {
|
||||
skeleton.overflowCache.forValue((overflowRow, overflowCol, value) => {
|
||||
if (Rectangle.contains(value, {
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
})) {
|
||||
targetRow = overflowRow;
|
||||
targetCol = overflowCol;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { cellHyperLinks, clickedHyperLink, customRange } = this._getCellHyperLinkInfo(
|
||||
worksheet,
|
||||
targetRow,
|
||||
targetCol,
|
||||
currentCell.customRange
|
||||
);
|
||||
if (!customRange) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const location = { unitId, subUnitId, row: targetRow, col: targetCol };
|
||||
const { viewPermission, editPermission, copyPermission } = this._getLinkPermission(location);
|
||||
if (!viewPermission) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const cellStyle = worksheet.getCellStyleOnly(targetRow, targetCol);
|
||||
const style = workbook.getStyles().getStyleByCell(cellStyle);
|
||||
const showAll = Boolean(style?.tr?.a) || (!clickedHyperLink && cellHyperLinks.length > 1);
|
||||
|
||||
this._sheetsHyperLinkPopupService.showPopup({
|
||||
...location,
|
||||
editPermission,
|
||||
copyPermission,
|
||||
customRange,
|
||||
customRangeRect: currentCell.rect,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
showAll,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private _initClickEditingListener(): void {
|
||||
let subscription: Nullable<Subscription> = null;
|
||||
this.disposeWithMe(
|
||||
this._editorBridgeService.currentEditCellState$
|
||||
.pipe(switchMap((state) => this._editorBridgeService.visible$.pipe(map((visible) => ({ visible, state })))))
|
||||
.subscribe(({ visible, state }) => {
|
||||
if (!state || state.editorUnitId !== DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visible.visible) {
|
||||
subscription?.unsubscribe();
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup(HyperLinkEditSourceType.EDITING);
|
||||
this._sheetsHyperLinkPopupService.endEditing(HyperLinkEditSourceType.EDITING);
|
||||
return;
|
||||
}
|
||||
|
||||
const { editorUnitId, unitId, sheetId, row, column } = state;
|
||||
const renderer = this._renderManagerService.getRenderUnitById(editorUnitId);
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { editPermission, viewPermission, copyPermission } = this._getLinkPermission({
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
row,
|
||||
col: column,
|
||||
});
|
||||
if (!viewPermission) {
|
||||
return;
|
||||
}
|
||||
|
||||
const docEventService = renderer.with(DocEventManagerService);
|
||||
subscription?.unsubscribe();
|
||||
subscription = docEventService.clickCustomRanges$.subscribe((customRange) => {
|
||||
if (customRange.range.rangeType !== CustomRangeType.HYPERLINK) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = customRange.rects[customRange.rects.length - 1];
|
||||
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
|
||||
?.with(SheetSkeletonManagerService)
|
||||
.getSkeletonParam(sheetId)
|
||||
?.skeleton;
|
||||
if (!skeleton || !rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasClientRect = renderer.engine.getCanvasElement().getBoundingClientRect();
|
||||
this._sheetsHyperLinkPopupService.showPopup({
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
row,
|
||||
col: column,
|
||||
customRange: customRange.range,
|
||||
customRangeRect: {
|
||||
left: rect.left + canvasClientRect.left,
|
||||
top: rect.top + canvasClientRect.top,
|
||||
bottom: rect.bottom + canvasClientRect.top,
|
||||
right: rect.right + canvasClientRect.left,
|
||||
},
|
||||
editPermission,
|
||||
copyPermission,
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(() => subscription?.unsubscribe());
|
||||
}
|
||||
|
||||
private _initTextSelectionListener(): void {
|
||||
this.disposeWithMe(this._textSelectionManagerService.textSelection$.subscribe((selection) => {
|
||||
if (selection?.unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
this._sheetsHyperLinkPopupService.endEditing(HyperLinkEditSourceType.EDITING);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _initCommandListener(): void {
|
||||
const hideCommandList = [
|
||||
ClearSelectionContentCommand.id,
|
||||
ClearSelectionAllCommand.id,
|
||||
ClearSelectionFormatCommand.id,
|
||||
];
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command) => {
|
||||
if (hideCommandList.includes(command.id)) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 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 { Nullable, Workbook } from '@univerjs/core';
|
||||
import type { ISheetLocationBase } from '@univerjs/sheets';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { CustomRangeType, Disposable, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService, Inject, IPermissionService, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DocEventManagerService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
ClearSelectionAllCommand,
|
||||
ClearSelectionContentCommand,
|
||||
ClearSelectionFormatCommand,
|
||||
RangeProtectionPermissionEditPoint,
|
||||
RangeProtectionPermissionViewPoint,
|
||||
SheetPermissionCheckController,
|
||||
WorkbookCopyPermission,
|
||||
WorkbookEditablePermission,
|
||||
WorkbookViewPermission,
|
||||
WorksheetCopyPermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetInsertHyperlinkPermission,
|
||||
WorksheetViewPermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { HoverManagerService, HoverRenderController, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { debounceTime, map, switchMap } from 'rxjs';
|
||||
import { ISheetsHyperLinkPopupService } from '../services/popup.service';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
|
||||
export class SheetsHyperLinkPopupController extends Disposable {
|
||||
constructor(
|
||||
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
|
||||
@Inject(ISheetsHyperLinkPopupService) private readonly _sheetsHyperLinkPopupService: ISheetsHyperLinkPopupService,
|
||||
@Inject(IRenderManagerService) private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(IPermissionService) private readonly _permissionService: IPermissionService,
|
||||
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initHoverListener();
|
||||
this._initCommandListener();
|
||||
this._initHoverEditingListener();
|
||||
this._initTextSelectionListener();
|
||||
}
|
||||
|
||||
private _getLinkPermission(location: ISheetLocationBase) {
|
||||
const { unitId, subUnitId, row: currentRow, col: currentCol } = location;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) {
|
||||
return {
|
||||
viewPermission: false,
|
||||
editPermission: false,
|
||||
copyPermission: false,
|
||||
};
|
||||
}
|
||||
|
||||
const viewPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookViewPermission],
|
||||
worksheetTypes: [WorksheetViewPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}, [{ startRow: currentRow, startColumn: currentCol, endRow: currentRow, endColumn: currentCol }], unitId, subUnitId);
|
||||
|
||||
let editPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetInsertHyperlinkPermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}, [{ startRow: currentRow, startColumn: currentCol, endRow: currentRow, endColumn: currentCol }], unitId, subUnitId);
|
||||
|
||||
// Check if the cell is a hyperlink formula, if it is, we don't allow editing
|
||||
const cell = worksheet.getCellRaw(currentRow, currentCol);
|
||||
if (cell?.f && cell.f.startsWith('=HYPERLINK(')) {
|
||||
editPermission = false;
|
||||
}
|
||||
|
||||
const copyPermission = this._permissionService.composePermission([new WorkbookCopyPermission(unitId).id, new WorksheetCopyPermission(unitId, subUnitId).id]).every((permission) => permission.value);
|
||||
|
||||
return {
|
||||
viewPermission,
|
||||
editPermission,
|
||||
copyPermission,
|
||||
};
|
||||
}
|
||||
|
||||
private _initHoverListener() {
|
||||
this.disposeWithMe(
|
||||
// hover over not editing cell
|
||||
this._hoverManagerService.currentRichText$.pipe(debounceTime(200)).subscribe((currentCell) => {
|
||||
if (!currentCell || currentCell.customRange?.rangeType !== CustomRangeType.HYPERLINK) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
const { unitId, subUnitId, row, col } = currentCell;
|
||||
const renderer = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) {
|
||||
return;
|
||||
}
|
||||
const hoverRenderController = renderer.with(HoverRenderController);
|
||||
if (!hoverRenderController.active) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup(HyperLinkEditSourceType.VIEWING);
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = renderer?.with(SheetSkeletonManagerService)
|
||||
.getSkeletonParam(subUnitId)
|
||||
?.skeleton;
|
||||
|
||||
const currentCol = col;
|
||||
const currentRow = row;
|
||||
let targetRow = currentRow;
|
||||
let targetCol = currentCol;
|
||||
|
||||
if (skeleton) {
|
||||
skeleton.overflowCache.forValue((row, col, value) => {
|
||||
if (Rectangle.contains(value, { startColumn: currentCol, endColumn: currentCol, startRow: currentRow, endRow: currentRow })) {
|
||||
targetRow = row;
|
||||
targetCol = col;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { viewPermission, editPermission, copyPermission } = this._getLinkPermission(currentCell);
|
||||
|
||||
if (!viewPermission) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
const cell = worksheet.getCellStyleOnly(targetRow, targetCol);
|
||||
const style = workbook!.getStyles().getStyleByCell(cell);
|
||||
const tr = style?.tr?.a;
|
||||
|
||||
if (!tr && !currentCell.customRange) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
this._sheetsHyperLinkPopupService.showPopup({
|
||||
row: targetRow,
|
||||
col: targetCol,
|
||||
editPermission,
|
||||
copyPermission,
|
||||
customRange: currentCell.customRange,
|
||||
customRangeRect: currentCell.rect,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
unitId,
|
||||
subUnitId,
|
||||
showAll: Boolean(tr),
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initHoverEditingListener() {
|
||||
let subscribe: Nullable<Subscription> = null;
|
||||
this.disposeWithMe(
|
||||
this._editorBridgeService.currentEditCellState$
|
||||
.pipe(switchMap((state) => this._editorBridgeService.visible$.pipe(map((visible) => ({ visible, state })))))
|
||||
.subscribe(({ visible, state }) => {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (state.editorUnitId !== DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visible.visible) {
|
||||
subscribe?.unsubscribe();
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup(HyperLinkEditSourceType.EDITING);
|
||||
this._sheetsHyperLinkPopupService.endEditing(HyperLinkEditSourceType.EDITING);
|
||||
return;
|
||||
}
|
||||
|
||||
const { editorUnitId, unitId, sheetId, row, column } = state;
|
||||
const renderer = this._renderManagerService.getRenderUnitById(editorUnitId);
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
const { editPermission, viewPermission, copyPermission } = this._getLinkPermission({ unitId, subUnitId: sheetId, row, col: column });
|
||||
const docEventService = renderer.with(DocEventManagerService);
|
||||
if (!viewPermission) {
|
||||
return;
|
||||
}
|
||||
subscribe?.unsubscribe();
|
||||
subscribe = docEventService.hoverCustomRanges$.pipe(debounceTime(200)).subscribe((customRanges) => {
|
||||
const customRange = customRanges.find((customRange) => customRange.range.rangeType === CustomRangeType.HYPERLINK);
|
||||
if (!customRange) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
return;
|
||||
}
|
||||
const rect = customRange.rects[customRange.rects.length - 1];
|
||||
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
|
||||
?.with(SheetSkeletonManagerService)
|
||||
.getSkeletonParam(sheetId)
|
||||
?.skeleton;
|
||||
if (!skeleton || !rect) {
|
||||
return;
|
||||
}
|
||||
const canvasClientRect = renderer.engine.getCanvasElement().getBoundingClientRect();
|
||||
|
||||
this._sheetsHyperLinkPopupService.showPopup({
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
row,
|
||||
col: column,
|
||||
customRange: customRange.range,
|
||||
customRangeRect: {
|
||||
left: rect.left + canvasClientRect.left,
|
||||
top: rect.top + canvasClientRect.top,
|
||||
bottom: rect.bottom + canvasClientRect.top,
|
||||
right: rect.right + canvasClientRect.left,
|
||||
},
|
||||
editPermission,
|
||||
copyPermission,
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(() => {
|
||||
subscribe?.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
private _initTextSelectionListener() {
|
||||
this.disposeWithMe(
|
||||
this._textSelectionManagerService.textSelection$.subscribe((selection) => {
|
||||
if (selection && selection.unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
this._sheetsHyperLinkPopupService.endEditing(HyperLinkEditSourceType.EDITING);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initCommandListener() {
|
||||
const HIDE_COMMAND_LIST = [ClearSelectionContentCommand.id, ClearSelectionAllCommand.id, ClearSelectionFormatCommand.id];
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command) => {
|
||||
if (HIDE_COMMAND_LIST.includes(command.id)) {
|
||||
this._sheetsHyperLinkPopupService.hideCurrentPopup();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 { Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { HyperLinkModel } from '@univerjs/sheets-hyper-link';
|
||||
import { debounceTime } from 'rxjs';
|
||||
|
||||
export class SheetsHyperLinkRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(HyperLinkModel) private readonly _hyperLinkModel: HyperLinkModel
|
||||
) {
|
||||
super();
|
||||
this._initSkeletonChange();
|
||||
}
|
||||
|
||||
private _initSkeletonChange() {
|
||||
const markSkeletonDirty = () => {
|
||||
this._context.mainComponent?.makeForceDirty();
|
||||
};
|
||||
|
||||
this.disposeWithMe(this._hyperLinkModel.linkUpdate$.pipe(debounceTime(16)).subscribe(() => {
|
||||
markSkeletonDirty();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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, Inject } from '@univerjs/core';
|
||||
import { IMenuManagerService, IShortcutService } from '@univerjs/ui';
|
||||
import {
|
||||
CloseHyperLinkPopupOperation,
|
||||
InsertHyperLinkOperation,
|
||||
InsertHyperLinkToolbarOperation,
|
||||
OpenHyperLinkEditPanelOperation,
|
||||
} from '../commands/operations/popup.operations';
|
||||
import { InsertLinkShortcut } from '../menu/menu';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
|
||||
export class SheetsHyperLinkUIController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private _commandService: ICommandService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@Inject(IShortcutService) private _shortcutService: IShortcutService
|
||||
) {
|
||||
super();
|
||||
this._initCommands();
|
||||
this._initMenus();
|
||||
this._initShortCut();
|
||||
}
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
OpenHyperLinkEditPanelOperation,
|
||||
CloseHyperLinkPopupOperation,
|
||||
InsertHyperLinkOperation,
|
||||
InsertHyperLinkToolbarOperation,
|
||||
].forEach((command) => {
|
||||
this._commandService.registerCommand(command);
|
||||
});
|
||||
}
|
||||
|
||||
private _initMenus() {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _initShortCut() {
|
||||
this._shortcutService.registerShortcut(InsertLinkShortcut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link';
|
||||
import { SheetsHyperLinkResolverService } from '../services/resolver.service';
|
||||
|
||||
export class SheetHyperLinkUrlController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetsHyperLinkParserService) private readonly _parserService: SheetsHyperLinkParserService,
|
||||
@Inject(SheetsHyperLinkResolverService) private _resolverService: SheetsHyperLinkResolverService
|
||||
) {
|
||||
super();
|
||||
this._handleInitUrl();
|
||||
}
|
||||
|
||||
private _handleInitUrl() {
|
||||
const hash = location.hash;
|
||||
if (hash) {
|
||||
const linkInfo = this._parserService.parseHyperLink(hash);
|
||||
this._resolverService.navigate(linkInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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, IWorkbookData, UnitModel } from '@univerjs/core';
|
||||
import type { IMessageProps } from '@univerjs/design';
|
||||
import {
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import {
|
||||
DefinedNamesService,
|
||||
FormulaDataModel,
|
||||
FunctionService,
|
||||
IDefinedNamesService,
|
||||
IFunctionService,
|
||||
ISuperTableService,
|
||||
LexerTreeBuilder,
|
||||
SuperTableService,
|
||||
} from '@univerjs/engine-formula';
|
||||
import {
|
||||
RefRangeService,
|
||||
SetWorksheetActiveOperation,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link';
|
||||
import { SheetsHyperLinkResolverService } from '@univerjs/sheets-hyper-link-ui';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-hyper-link-ui/facade';
|
||||
|
||||
class TestMessageService {
|
||||
readonly messages: IMessageProps[] = [];
|
||||
|
||||
show(options: IMessageProps): void {
|
||||
this.messages.push(options);
|
||||
}
|
||||
}
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'Workbook',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
cellData: {},
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Sheet2',
|
||||
cellData: {},
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHyperLinkFacadeTestBed(workbookData: IWorkbookData, dependencies: Dependency[]) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([RefRangeService]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([ISuperTableService, { useClass: SuperTableService }]);
|
||||
dependencies.forEach((dependency) => injector.add(dependency));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData);
|
||||
injector.get(IUniverInstanceService).focusUnit(sheet.getUnitId());
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
univerAPI: FUniver.newAPI(injector),
|
||||
};
|
||||
}
|
||||
|
||||
describe('sheets-hyper-link-ui workbook facade', () => {
|
||||
let testBed: ReturnType<typeof createHyperLinkFacadeTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
const dependencies: Dependency[] = [
|
||||
[SheetsHyperLinkParserService],
|
||||
[SheetsHyperLinkResolverService],
|
||||
[IMessageService, { useClass: TestMessageService }],
|
||||
];
|
||||
testBed = createHyperLinkFacadeTestBed(createWorkbookData(), dependencies);
|
||||
testBed.get(ICommandService).registerCommand(SetWorksheetActiveOperation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('navigates hyperlinks through the workbook facade API', async () => {
|
||||
const workbook = testBed.univerAPI.getActiveWorkbook()!;
|
||||
const open = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
||||
workbook.navigateToSheetHyperlink('https://univer.ai');
|
||||
await Promise.resolve();
|
||||
|
||||
expect(open).toHaveBeenCalledWith('https://univer.ai', '_blank', 'noopener noreferrer');
|
||||
open.mockRestore();
|
||||
});
|
||||
});
|
||||
63
packages/sheets-hyper-link-ui/src/facade/f-workbook.ts
Normal file
63
packages/sheets-hyper-link-ui/src/facade/f-workbook.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link';
|
||||
import { SheetsHyperLinkResolverService } from '@univerjs/sheets-hyper-link-ui';
|
||||
import { FWorkbook } from '@univerjs/sheets/facade';
|
||||
|
||||
interface IFWorkbookHyperlinkUIMixin {
|
||||
/**
|
||||
* Navigate to the sheet hyperlink.
|
||||
* @param {string} hyperlink - The hyperlink string
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const sheets = fWorkbook.getSheets();
|
||||
*
|
||||
* // Create a hyperlink to the cell F6 in the first sheet
|
||||
* const sheet1 = sheets[0];
|
||||
* const range = sheet1.getRange('F6');
|
||||
* const hyperlink = range.getUrl();
|
||||
*
|
||||
* // Switch to the second sheet
|
||||
* fWorkbook.setActiveSheet(sheets[1]);
|
||||
* console.log(fWorkbook.getActiveSheet().getSheetName());
|
||||
*
|
||||
* // Navigate to the hyperlink after 3 seconds
|
||||
* setTimeout(() => {
|
||||
* fWorkbook.navigateToSheetHyperlink(hyperlink);
|
||||
* console.log(fWorkbook.getActiveSheet().getSheetName());
|
||||
* }, 3000);
|
||||
* ```
|
||||
*/
|
||||
navigateToSheetHyperlink(this: FWorkbook, hyperlink: string): void;
|
||||
}
|
||||
|
||||
class FWorkbookHyperlinkUIMixin extends FWorkbook implements IFWorkbookHyperlinkUIMixin {
|
||||
// TODO: this should be migrated back to hyperlink ui plugin
|
||||
override navigateToSheetHyperlink(hyperlink: string): void {
|
||||
const parserService = this._injector.get(SheetsHyperLinkParserService);
|
||||
const resolverService = this._injector.get(SheetsHyperLinkResolverService);
|
||||
const info = parserService.parseHyperLink(hyperlink);
|
||||
resolverService.navigate(info);
|
||||
}
|
||||
}
|
||||
|
||||
FWorkbook.extend(FWorkbookHyperlinkUIMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FWorkbook extends IFWorkbookHyperlinkUIMixin {}
|
||||
}
|
||||
17
packages/sheets-hyper-link-ui/src/facade/index.ts
Normal file
17
packages/sheets-hyper-link-ui/src/facade/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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-workbook';
|
||||
2
packages/sheets-hyper-link-ui/src/global.css
Normal file
2
packages/sheets-hyper-link-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
37
packages/sheets-hyper-link-ui/src/index.ts
Normal file
37
packages/sheets-hyper-link-ui/src/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 {
|
||||
CloseHyperLinkPopupOperation,
|
||||
InsertHyperLinkOperation,
|
||||
OpenHyperLinkEditPanelOperation,
|
||||
} from './commands/operations/popup.operations';
|
||||
export type { IOpenHyperLinkEditPanelOperationParams } from './commands/operations/popup.operations';
|
||||
export type { IUniverSheetsHyperLinkUIConfig } from './config/config';
|
||||
export { SheetsHyperLinkCopyPasteController } from './controllers/copy-paste.controller';
|
||||
export { InsertLinkShortcut } from './menu/menu';
|
||||
export { menuSchema as SheetsHyperLinkUIMenuSchema } from './menu/schema';
|
||||
export { UniverSheetsHyperLinkMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverSheetsHyperLinkUIPlugin } from './plugin';
|
||||
export {
|
||||
ISheetsHyperLinkPopupService,
|
||||
SheetsHyperLinkPopupService,
|
||||
} from './services/popup.service';
|
||||
export { SheetsHyperLinkResolverService } from './services/resolver.service';
|
||||
export { SheetsHyperLinkSidePanelService } from './services/side-panel.service';
|
||||
export type { ICustomHyperLinkView } from './services/side-panel.service';
|
||||
59
packages/sheets-hyper-link-ui/src/locale/ar-SA.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'إدراج ارتباط تشعبي',
|
||||
label: 'التسمية',
|
||||
type: 'النوع',
|
||||
link: 'الرابط',
|
||||
linkPlaceholder: 'أدخل الرابط',
|
||||
range: 'النطاق',
|
||||
worksheet: 'ورقة العمل',
|
||||
definedName: 'الاسم المعرف',
|
||||
ok: 'تأكيد',
|
||||
cancel: 'إلغاء',
|
||||
labelPlaceholder: 'أدخل التسمية',
|
||||
inputError: 'الرجاء الإدخال',
|
||||
selectError: 'الرجاء التحديد',
|
||||
linkError: 'الرجاء إدخال رابط صالح',
|
||||
},
|
||||
menu: {
|
||||
add: 'إدراج ارتباط تشعبي',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'ليس لديك إذن لإدراج رابط.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'تم حذف الورقة المستهدفة',
|
||||
refError: 'نطاق غير صالح',
|
||||
hiddenSheet: 'تعذر فتح الرابط لأن الورقة المرتبطة مخفية',
|
||||
coped: 'تم نسخ الرابط إلى الحافظة',
|
||||
},
|
||||
popup: {
|
||||
title: 'ارتباط تشعبي',
|
||||
open: 'فتح الرابط',
|
||||
copy: 'نسخ الرابط',
|
||||
edit: 'تحرير الرابط',
|
||||
cancel: 'إلغاء الارتباط',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/ca-ES.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Insereix enllaç',
|
||||
label: 'Etiqueta',
|
||||
type: 'Tipus',
|
||||
link: 'Enllaç',
|
||||
linkPlaceholder: 'Introdueix l\'enllaç',
|
||||
range: 'Interval',
|
||||
worksheet: 'Full de càlcul',
|
||||
definedName: 'Nom definit',
|
||||
ok: 'Confirmar',
|
||||
cancel: 'Cancel·lar',
|
||||
labelPlaceholder: 'Introdueix l\'etiqueta',
|
||||
inputError: 'Si us plau, introdueix',
|
||||
selectError: 'Si us plau, selecciona',
|
||||
linkError: 'Si us plau, introdueix un enllaç vàlid',
|
||||
},
|
||||
menu: {
|
||||
add: 'Insereix enllaç',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'No teniu permís per inserir un enllaç.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'El full de destinació ha estat eliminat',
|
||||
refError: 'Interval no vàlid',
|
||||
hiddenSheet: 'No es pot obrir l\'enllaç perquè el full enllaçat està amagat',
|
||||
coped: 'Enllaç copiat al porta-retalls',
|
||||
},
|
||||
popup: {
|
||||
title: 'Enllaç',
|
||||
open: 'Obre l\'enllaç',
|
||||
copy: 'Copia l\'enllaç',
|
||||
edit: 'Edita l\'enllaç',
|
||||
cancel: 'Cancel·la l\'enllaç',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/de-DE.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Link einfügen',
|
||||
label: 'Beschriftung',
|
||||
type: 'Typ',
|
||||
link: 'Link',
|
||||
linkPlaceholder: 'Link eingeben',
|
||||
range: 'Bereich',
|
||||
worksheet: 'Arbeitsblatt',
|
||||
definedName: 'Definierter Name',
|
||||
ok: 'Bestätigen',
|
||||
cancel: 'Abbrechen',
|
||||
labelPlaceholder: 'Beschriftung eingeben',
|
||||
inputError: 'Bitte eingeben',
|
||||
selectError: 'Bitte auswählen',
|
||||
linkError: 'Bitte einen gültigen Link eingeben',
|
||||
},
|
||||
menu: {
|
||||
add: 'Link einfügen',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Sie haben keine Berechtigung, einen Link einzufügen.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Zielblatt wurde gelöscht',
|
||||
refError: 'Ungültiger Bereich',
|
||||
hiddenSheet: 'Der Link kann nicht geöffnet werden, da das verknüpfte Blatt ausgeblendet ist',
|
||||
coped: 'Link in Zwischenablage kopiert',
|
||||
},
|
||||
popup: {
|
||||
title: 'Hyperlink',
|
||||
open: 'Link öffnen',
|
||||
copy: 'Link kopieren',
|
||||
edit: 'Link bearbeiten',
|
||||
cancel: 'Link aufheben',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
57
packages/sheets-hyper-link-ui/src/locale/en-US.ts
Normal file
57
packages/sheets-hyper-link-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Insert Link',
|
||||
label: 'Label',
|
||||
type: 'Type',
|
||||
link: 'Link',
|
||||
linkPlaceholder: 'Enter link',
|
||||
range: 'Range',
|
||||
worksheet: 'Worksheet',
|
||||
definedName: 'Defined Name',
|
||||
ok: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
labelPlaceholder: 'Enter label',
|
||||
inputError: 'Please enter',
|
||||
selectError: 'Please select',
|
||||
linkError: 'Please enter a legal link',
|
||||
},
|
||||
menu: {
|
||||
add: 'Insert Link',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'You do not have permission to insert a link.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Target sheet has been delete',
|
||||
refError: 'Invalid Range',
|
||||
hiddenSheet: 'Cannot open the link because the linked sheet is hidden',
|
||||
coped: 'Link copied to clipboard',
|
||||
},
|
||||
popup: {
|
||||
title: 'Hyperlink',
|
||||
open: 'Open Link',
|
||||
copy: 'Copy Link',
|
||||
edit: 'Edit Link',
|
||||
cancel: 'Cancel Link',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/es-ES.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Insertar enlace',
|
||||
label: 'Etiqueta',
|
||||
type: 'Tipo',
|
||||
link: 'Enlace',
|
||||
linkPlaceholder: 'Introduce el enlace',
|
||||
range: 'Rango',
|
||||
worksheet: 'Hoja de cálculo',
|
||||
definedName: 'Nombre definido',
|
||||
ok: 'Confirmar',
|
||||
cancel: 'Cancelar',
|
||||
labelPlaceholder: 'Introduce la etiqueta',
|
||||
inputError: 'Por favor, introduce',
|
||||
selectError: 'Por favor, selecciona',
|
||||
linkError: 'Por favor, introduce un enlace válido',
|
||||
},
|
||||
menu: {
|
||||
add: 'Insertar enlace',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'No tiene permiso para insertar un enlace.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'La hoja de destino ha sido eliminada',
|
||||
refError: 'Rango no válido',
|
||||
hiddenSheet: 'No se puede abrir el enlace porque la hoja enlazada está oculta',
|
||||
coped: 'Enlace copiado al portapapeles',
|
||||
},
|
||||
popup: {
|
||||
title: 'Hipervínculo',
|
||||
open: 'Abrir enlace',
|
||||
copy: 'Copiar enlace',
|
||||
edit: 'Editar enlace',
|
||||
cancel: 'Cancelar enlace',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/fa-IR.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'درج لینک',
|
||||
label: 'برچسب',
|
||||
type: 'نوع',
|
||||
link: 'لینک',
|
||||
linkPlaceholder: 'لینک را وارد کنید',
|
||||
range: 'محدوده',
|
||||
worksheet: 'برگ کار',
|
||||
definedName: 'نام تعریف شده',
|
||||
ok: 'تایید',
|
||||
cancel: 'انصراف',
|
||||
labelPlaceholder: 'برچسب را وارد کنید',
|
||||
inputError: 'لطفا وارد کنید',
|
||||
selectError: 'لطفا انتخاب کنید',
|
||||
linkError: 'لطفا یک لینک معتبر وارد کنید',
|
||||
},
|
||||
menu: {
|
||||
add: 'درج لینک',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'شما مجوز درج لینک را ندارید.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'برگ هدف حذف شده است',
|
||||
refError: 'محدوده نامعتبر',
|
||||
hiddenSheet: 'نمیتوان لینک را باز کرد زیرا برگ مرتبط پنهان است',
|
||||
coped: 'لینک به کلیپ بورد کپی شد',
|
||||
},
|
||||
popup: {
|
||||
title: 'ابرپیوند',
|
||||
open: 'باز کردن لینک',
|
||||
copy: 'کپی لینک',
|
||||
edit: 'ویرایش لینک',
|
||||
cancel: 'لینک را لغو کنید',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/fr-FR.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Insérer un lien',
|
||||
label: 'Titre',
|
||||
type: 'Type',
|
||||
link: 'Lien',
|
||||
linkPlaceholder: 'Entrez le lien',
|
||||
range: 'Plage',
|
||||
worksheet: 'Feuille de calcul',
|
||||
definedName: 'Nom défini',
|
||||
ok: 'Confirmer',
|
||||
cancel: 'Annuler',
|
||||
labelPlaceholder: 'Entrez le titre',
|
||||
inputError: 'Veuillez entrer',
|
||||
selectError: 'Veuillez sélectionner',
|
||||
linkError: 'Veuillez entrer un lien valide',
|
||||
},
|
||||
menu: {
|
||||
add: 'Insérer un lien',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Vous n’avez pas l’autorisation d’insérer un lien.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'La feuille cible a été supprimée',
|
||||
refError: 'Plage invalide',
|
||||
hiddenSheet: 'Impossible d\'ouvrir le lien car la feuille liée est masquée',
|
||||
coped: 'Lien copié dans le presse-papiers',
|
||||
},
|
||||
popup: {
|
||||
title: 'Lien hypertexte',
|
||||
open: 'Ouvrir le lien',
|
||||
copy: 'Copier le lien',
|
||||
edit: 'Modifier le lien',
|
||||
cancel: 'Annuler le lien',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/id-ID.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Sisipkan Tautan',
|
||||
label: 'Label',
|
||||
type: 'Tipe',
|
||||
link: 'Tautan',
|
||||
linkPlaceholder: 'Masukkan tautan',
|
||||
range: 'Rentang',
|
||||
worksheet: 'Lembar Kerja',
|
||||
definedName: 'Nama yang Ditentukan',
|
||||
ok: 'Konfirmasi',
|
||||
cancel: 'Batal',
|
||||
labelPlaceholder: 'Masukkan label',
|
||||
inputError: 'Silakan masukkan',
|
||||
selectError: 'Silakan pilih',
|
||||
linkError: 'Silakan masukkan tautan yang valid',
|
||||
},
|
||||
menu: {
|
||||
add: 'Sisipkan Tautan',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Anda tidak memiliki izin untuk menyisipkan tautan.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Lembar target telah dihapus',
|
||||
refError: 'Rentang Tidak Valid',
|
||||
hiddenSheet: 'Tidak dapat membuka tautan karena lembar yang ditautkan tersembunyi',
|
||||
coped: 'Tautan disalin ke clipboard',
|
||||
},
|
||||
popup: {
|
||||
title: 'Tautan',
|
||||
open: 'Buka Tautan',
|
||||
copy: 'Salin Tautan',
|
||||
edit: 'Edit Tautan',
|
||||
cancel: 'Batalkan Tautan',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/it-IT.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Inserisci collegamento',
|
||||
label: 'Etichetta',
|
||||
type: 'Tipo',
|
||||
link: 'Collegamento',
|
||||
linkPlaceholder: 'Inserisci collegamento',
|
||||
range: 'Intervallo',
|
||||
worksheet: 'Foglio di lavoro',
|
||||
definedName: 'Nome definito',
|
||||
ok: 'Conferma',
|
||||
cancel: 'Annulla',
|
||||
labelPlaceholder: 'Inserisci etichetta',
|
||||
inputError: 'Inserisci',
|
||||
selectError: 'Seleziona',
|
||||
linkError: 'Inserisci un collegamento valido',
|
||||
},
|
||||
menu: {
|
||||
add: 'Inserisci collegamento',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Non hai l’autorizzazione per inserire un collegamento.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Il foglio di destinazione è stato eliminato',
|
||||
refError: 'Intervallo non valido',
|
||||
hiddenSheet: 'Impossibile aprire il collegamento perché il foglio collegato è nascosto',
|
||||
coped: 'Collegamento copiato negli appunti',
|
||||
},
|
||||
popup: {
|
||||
title: 'Collegamento ipertestuale',
|
||||
open: 'Apri collegamento',
|
||||
copy: 'Copia collegamento',
|
||||
edit: 'Modifica collegamento',
|
||||
cancel: 'Annulla collegamento',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/ja-JP.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'リンクの挿入',
|
||||
label: '表示文字列',
|
||||
type: '種類',
|
||||
link: 'リンク',
|
||||
linkPlaceholder: 'リンクを入力',
|
||||
range: '範囲',
|
||||
worksheet: 'ワークシート',
|
||||
definedName: '定義済みの名前',
|
||||
ok: 'OK',
|
||||
cancel: 'キャンセル',
|
||||
labelPlaceholder: '表示する文字列を入力',
|
||||
inputError: '値を入力してください',
|
||||
selectError: '選択してください',
|
||||
linkError: '有効なリンクを入力してください',
|
||||
},
|
||||
menu: {
|
||||
add: 'リンクの挿入',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'リンクを挿入する権限がありません。',
|
||||
},
|
||||
message: {
|
||||
noSheet: '対象のワークシートが削除されました',
|
||||
refError: '無効な範囲です',
|
||||
hiddenSheet: 'リンクを開けません。参照先のワークシートが非表示です',
|
||||
coped: 'リンクがクリップボードにコピーされました',
|
||||
},
|
||||
popup: {
|
||||
title: 'ハイパーリンク',
|
||||
open: 'リンクを開く',
|
||||
copy: 'リンクをコピー',
|
||||
edit: 'リンクの編集',
|
||||
cancel: 'リンク解除',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/ko-KR.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: '링크 삽입',
|
||||
label: '표시 텍스트',
|
||||
type: '유형',
|
||||
link: '링크',
|
||||
linkPlaceholder: '링크 입력',
|
||||
range: '범위',
|
||||
worksheet: '워크시트',
|
||||
definedName: '정의된 이름',
|
||||
ok: '확인',
|
||||
cancel: '취소',
|
||||
labelPlaceholder: '표시할 텍스트 입력',
|
||||
inputError: '값을 입력하세요',
|
||||
selectError: '선택하세요',
|
||||
linkError: '유효한 링크를 입력하세요',
|
||||
},
|
||||
menu: {
|
||||
add: '링크 삽입',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: '링크를 삽입할 권한이 없습니다.',
|
||||
},
|
||||
message: {
|
||||
noSheet: '대상 워크시트가 삭제되었습니다',
|
||||
refError: '잘못된 범위입니다',
|
||||
hiddenSheet: '링크를 열 수 없습니다. 연결된 워크시트가 숨겨져 있습니다',
|
||||
coped: '링크가 클립보드에 복사되었습니다',
|
||||
},
|
||||
popup: {
|
||||
title: '하이퍼링크',
|
||||
open: '링크 열기',
|
||||
copy: '링크 복사',
|
||||
edit: '링크 편집',
|
||||
cancel: '링크 취소',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/pl-PL.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Wstaw łącze',
|
||||
label: 'Etykieta',
|
||||
type: 'Typ',
|
||||
link: 'Łącze',
|
||||
linkPlaceholder: 'Wprowadź łącze',
|
||||
range: 'Zakres',
|
||||
worksheet: 'Arkusz',
|
||||
definedName: 'Nazwa zdefiniowana',
|
||||
ok: 'Potwierdź',
|
||||
cancel: 'Anuluj',
|
||||
labelPlaceholder: 'Wprowadź etykietę',
|
||||
inputError: 'Wprowadź wartość',
|
||||
selectError: 'Wybierz wartość',
|
||||
linkError: 'Wprowadź prawidłowe łącze',
|
||||
},
|
||||
menu: {
|
||||
add: 'Wstaw łącze',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Nie masz uprawnień do wstawiania łącza.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Arkusz docelowy został usunięty',
|
||||
refError: 'Nieprawidłowy zakres',
|
||||
hiddenSheet: 'Nie można otworzyć łącza, ponieważ połączony arkusz jest ukryty',
|
||||
coped: 'Łącze skopiowane do schowka',
|
||||
},
|
||||
popup: {
|
||||
title: 'Hiperłącze',
|
||||
open: 'Otwórz łącze',
|
||||
copy: 'Kopiuj łącze',
|
||||
edit: 'Edytuj łącze',
|
||||
cancel: 'Anuluj łącze',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/pt-BR.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Inserir Link',
|
||||
label: 'Rótulo',
|
||||
type: 'Tipo',
|
||||
link: 'Link',
|
||||
linkPlaceholder: 'Digite o link',
|
||||
range: 'Intervalo',
|
||||
worksheet: 'Planilha',
|
||||
definedName: 'Nome Definido',
|
||||
ok: 'Confirmar',
|
||||
cancel: 'Cancelar',
|
||||
labelPlaceholder: 'Digite o rótulo',
|
||||
inputError: 'Por favor, digite',
|
||||
selectError: 'Por favor, selecione',
|
||||
linkError: 'Por favor, digite um link válido',
|
||||
},
|
||||
menu: {
|
||||
add: 'Inserir Link',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Você não tem permissão para inserir um link.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'A planilha de destino foi excluída',
|
||||
refError: 'Intervalo Inválido',
|
||||
hiddenSheet: 'Não é possível abrir o link porque a planilha vinculada está oculta',
|
||||
coped: 'Link copiado para a área de transferência',
|
||||
},
|
||||
popup: {
|
||||
title: 'Hiperlink',
|
||||
open: 'Abrir Link',
|
||||
copy: 'Copiar Link',
|
||||
edit: 'Editar Link',
|
||||
cancel: 'Cancelar Link',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/ru-RU.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Вставить ссылку',
|
||||
label: 'Метка',
|
||||
type: 'Тип',
|
||||
link: 'Ссылка',
|
||||
linkPlaceholder: 'Введите ссылку',
|
||||
range: 'Диапазон',
|
||||
worksheet: 'Лист',
|
||||
definedName: 'Определенное имя',
|
||||
ok: 'ОК',
|
||||
cancel: 'Отмена',
|
||||
labelPlaceholder: 'Введите метку',
|
||||
inputError: 'Пожалуйста, введите',
|
||||
selectError: 'Пожалуйста, выберите',
|
||||
linkError: 'Пожалуйста, введите корректную ссылку',
|
||||
},
|
||||
menu: {
|
||||
add: 'Вставить ссылку',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'У вас нет разрешения на вставку ссылки.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Целевой лист был удален',
|
||||
refError: 'Недопустимый диапазон',
|
||||
hiddenSheet: 'Невозможно открыть ссылку, так как связанный лист скрыт',
|
||||
coped: 'Ссылка скопирована в буфер обмена',
|
||||
},
|
||||
popup: {
|
||||
title: 'Гиперссылка',
|
||||
open: 'Открыть ссылку',
|
||||
copy: 'Копировать ссылку',
|
||||
edit: 'Редактировать ссылку',
|
||||
cancel: 'Отменить ссылку',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/sk-SK.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Vložiť odkaz',
|
||||
label: 'Popisok',
|
||||
type: 'Typ',
|
||||
link: 'Odkaz',
|
||||
linkPlaceholder: 'Zadajte odkaz',
|
||||
range: 'Rozsah',
|
||||
worksheet: 'Hárok',
|
||||
definedName: 'Definovaný názov',
|
||||
ok: 'Potvrdiť',
|
||||
cancel: 'Zrušiť',
|
||||
labelPlaceholder: 'Zadajte popisok',
|
||||
inputError: 'Zadajte hodnotu',
|
||||
selectError: 'Vyberte',
|
||||
linkError: 'Zadajte platný odkaz',
|
||||
},
|
||||
menu: {
|
||||
add: 'Vložiť odkaz',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Nemáte povolenie vložiť odkaz.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Cieľový hárok bol odstránený',
|
||||
refError: 'Neplatný rozsah',
|
||||
hiddenSheet: 'Odkaz nie je možné otvoriť, pretože prepojený hárok je skrytý',
|
||||
coped: 'Odkaz skopírovaný do schránky',
|
||||
},
|
||||
popup: {
|
||||
title: 'Hypertextový odkaz',
|
||||
open: 'Otvoriť odkaz',
|
||||
copy: 'Kopírovať odkaz',
|
||||
edit: 'Upraviť odkaz',
|
||||
cancel: 'Zrušiť odkaz',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-hyper-link-ui/src/locale/types.ts
Normal file
20
packages/sheets-hyper-link-ui/src/locale/types.ts
Normal 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>;
|
||||
60
packages/sheets-hyper-link-ui/src/locale/vi-VN.ts
Normal file
60
packages/sheets-hyper-link-ui/src/locale/vi-VN.ts
Normal 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: 'Chèn liên kết',
|
||||
label: 'Văn bản',
|
||||
labelPlaceholder: 'Nhập văn bản',
|
||||
type: 'Loại',
|
||||
link: 'Liên kết',
|
||||
linkPlaceholder: 'Nhập địa chỉ liên kết',
|
||||
range: 'Ô',
|
||||
worksheet: 'Bảng tính',
|
||||
definedName: 'Tên đã xác định',
|
||||
ok: 'Xác nhận',
|
||||
cancel: 'Hủy',
|
||||
inputError: 'Vui lòng nhập',
|
||||
selectError: 'Vui lòng chọn',
|
||||
linkError: 'Vui lòng nhập liên kết hợp lệ',
|
||||
},
|
||||
menu: {
|
||||
add: 'Thêm liên kết',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: 'Bạn không có quyền chèn liên kết.',
|
||||
},
|
||||
message: {
|
||||
noSheet: 'Bảng con này đã bị xóa',
|
||||
refError: 'Tham chiếu lỗi',
|
||||
hiddenSheet: 'Không thể mở bảng con bị ẩn',
|
||||
coped: 'Liên kết đã được sao chép vào clipboard',
|
||||
},
|
||||
popup: {
|
||||
title: 'Siêu liên kết',
|
||||
open: 'Mở liên kết',
|
||||
copy: 'Sao chép',
|
||||
edit: 'Chỉnh sửa',
|
||||
cancel: 'Hủy liên kết',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/zh-CN.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: '插入链接',
|
||||
label: '文本',
|
||||
labelPlaceholder: '输入文本',
|
||||
type: '类型',
|
||||
link: '链接',
|
||||
linkPlaceholder: '输入链接地址',
|
||||
range: '单元格',
|
||||
worksheet: '工作表',
|
||||
definedName: '定义的名称',
|
||||
ok: '确认',
|
||||
cancel: '取消',
|
||||
inputError: '请输入',
|
||||
selectError: '请选择',
|
||||
linkError: '请输入合法的链接',
|
||||
},
|
||||
menu: {
|
||||
add: '添加链接',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: '你没有权限插入链接。',
|
||||
},
|
||||
message: {
|
||||
noSheet: '该子表已被删除',
|
||||
refError: '错误的引用',
|
||||
hiddenSheet: '无法打开被隐藏的子表',
|
||||
coped: '链接已被复制到剪贴板',
|
||||
},
|
||||
popup: {
|
||||
title: '超链接',
|
||||
open: '打开链接',
|
||||
copy: '复制',
|
||||
edit: '编辑',
|
||||
cancel: '取消链接',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/zh-HK.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: '插入連結',
|
||||
label: '文字',
|
||||
labelPlaceholder: '輸入文字',
|
||||
type: '類型',
|
||||
link: '連結',
|
||||
linkPlaceholder: '輸入連結位址',
|
||||
range: '儲存格',
|
||||
worksheet: '工作表',
|
||||
definedName: '定義的名稱',
|
||||
ok: '確認',
|
||||
cancel: '取消',
|
||||
inputError: '請輸入',
|
||||
selectError: '請選擇',
|
||||
linkError: '請輸入合法的連結',
|
||||
},
|
||||
menu: {
|
||||
add: '新增連結',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: '你沒有權限插入連結。',
|
||||
},
|
||||
message: {
|
||||
noSheet: '該子表已被刪除',
|
||||
refError: '錯誤的引用',
|
||||
hiddenSheet: '無法開啟被隱藏的子表',
|
||||
coped: '連結已複製到剪貼簿',
|
||||
},
|
||||
popup: {
|
||||
title: '超連結',
|
||||
open: '開啟連結',
|
||||
copy: '複製',
|
||||
edit: '編輯',
|
||||
cancel: '取消連結',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
59
packages/sheets-hyper-link-ui/src/locale/zh-TW.ts
Normal file
59
packages/sheets-hyper-link-ui/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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-hyper-link-ui': {
|
||||
form: {
|
||||
addTitle: '插入連結',
|
||||
label: '文字',
|
||||
labelPlaceholder: '輸入文字',
|
||||
type: '類型',
|
||||
link: '連結',
|
||||
linkPlaceholder: '輸入連結位址',
|
||||
range: '儲存格',
|
||||
worksheet: '工作表',
|
||||
definedName: '定義的名稱',
|
||||
ok: '確認',
|
||||
cancel: '取消',
|
||||
inputError: '請輸入',
|
||||
selectError: '請選擇',
|
||||
linkError: '請輸入合法的連結',
|
||||
},
|
||||
menu: {
|
||||
add: '新增連結',
|
||||
},
|
||||
permission: {
|
||||
hyperLinkErr: '你沒有權限插入連結。',
|
||||
},
|
||||
message: {
|
||||
noSheet: '該子表已被刪除',
|
||||
refError: '錯誤的引用',
|
||||
hiddenSheet: '無法開啟被隱藏的子表',
|
||||
coped: '連結已複製到剪貼簿',
|
||||
},
|
||||
popup: {
|
||||
title: '超連結',
|
||||
open: '開啟連結',
|
||||
copy: '複製',
|
||||
edit: '編輯',
|
||||
cancel: '取消連結',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
160
packages/sheets-hyper-link-ui/src/menu/menu.ts
Normal file
160
packages/sheets-hyper-link-ui/src/menu/menu.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, IAccessor, Nullable } from '@univerjs/core';
|
||||
import type { IEditorBridgeServiceVisibleParam } from '@univerjs/sheets-ui';
|
||||
import type { IMenuItem, IShortcutItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
RangeProtectionPermissionEditPoint,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetInsertHyperlinkPermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { getCurrentRangeDisable$, IEditorBridgeService, whenSheetEditorFocused } from '@univerjs/sheets-ui';
|
||||
import { getMenuHiddenObservable, KeyCode, MenuItemType, MetaKeys } from '@univerjs/ui';
|
||||
import { combineLatest, map, of, switchMap } from 'rxjs';
|
||||
import { InsertHyperLinkOperation, InsertHyperLinkToolbarOperation } from '../commands/operations/popup.operations';
|
||||
import { DisableLinkType, getShouldDisableCellLink, shouldDisableAddLink } from '../utils';
|
||||
|
||||
const getEditingLinkDisable$ = (accessor: IAccessor, unitId = DOCS_NORMAL_EDITOR_UNIT_ID_KEY) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const docSelctionService = accessor.get(IRenderManagerService)
|
||||
.getRenderUnitById(unitId)
|
||||
?.with(DocSelectionRenderService);
|
||||
if (!docSelctionService) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return docSelctionService.textSelectionInner$.pipe(map(() => {
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
const state = editorBridgeService.getEditCellState();
|
||||
if (!state) {
|
||||
return true;
|
||||
}
|
||||
const target = getSheetCommandTarget(univerInstanceService, { unitId: state.unitId, subUnitId: state.sheetId });
|
||||
if (!target?.worksheet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (getShouldDisableCellLink(accessor, target.worksheet, state.row, state.column) === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return shouldDisableAddLink(accessor);
|
||||
}));
|
||||
};
|
||||
|
||||
const getLinkDisable$ = (accessor: IAccessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const editorBridgeService = accessor.has(IEditorBridgeService) ? accessor.get(IEditorBridgeService) : null;
|
||||
|
||||
const disableCell$ = editorBridgeService?.currentEditCellState$.pipe(
|
||||
map((state) => {
|
||||
if (!state) {
|
||||
return (DisableLinkType.DISABLED_BY_CELL);
|
||||
}
|
||||
const target = getSheetCommandTarget(univerInstanceService, {
|
||||
unitId: state.unitId,
|
||||
subUnitId: state.sheetId,
|
||||
});
|
||||
if (!target) {
|
||||
return (DisableLinkType.DISABLED_BY_CELL);
|
||||
}
|
||||
|
||||
return (getShouldDisableCellLink(accessor, target.worksheet, state.row, state.column));
|
||||
}),
|
||||
switchMap((disableCell) => {
|
||||
if (disableCell === DisableLinkType.DISABLED_BY_CELL) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
const isEditing$ = (editorBridgeService ? editorBridgeService.visible$ : of<Nullable<IEditorBridgeServiceVisibleParam>>(null));
|
||||
return combineLatest([
|
||||
isEditing$,
|
||||
univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC),
|
||||
]).pipe(
|
||||
switchMap(
|
||||
([editing, focusingDoc]) => {
|
||||
return editing?.visible ?
|
||||
focusingDoc?.getUnitId() === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY
|
||||
? of(true)
|
||||
: getEditingLinkDisable$(accessor, DOCS_NORMAL_EDITOR_UNIT_ID_KEY)
|
||||
: of(disableCell !== DisableLinkType.ALLOWED);
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
) ?? of(true);
|
||||
|
||||
return disableCell$.pipe(
|
||||
switchMap((disableCell) => {
|
||||
if (disableCell) {
|
||||
return of(true);
|
||||
} else {
|
||||
return getCurrentRangeDisable$(accessor, {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [
|
||||
WorksheetEditPermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
WorksheetInsertHyperlinkPermission,
|
||||
],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}, true);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const insertLinkMenuFactory = (accessor: IAccessor): IMenuItem<LocaleKey> => {
|
||||
return {
|
||||
commandId: InsertHyperLinkOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-hyper-link-ui.menu.add',
|
||||
icon: 'LinkIcon',
|
||||
id: InsertHyperLinkOperation.id,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: getLinkDisable$(accessor),
|
||||
};
|
||||
};
|
||||
|
||||
export const insertLinkMenuToolbarFactory = (accessor: IAccessor): IMenuItem<LocaleKey> => {
|
||||
return {
|
||||
tooltip: 'sheets-hyper-link-ui.form.addTitle',
|
||||
commandId: InsertHyperLinkToolbarOperation.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'LinkIcon',
|
||||
id: InsertHyperLinkToolbarOperation.id,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: getLinkDisable$(accessor),
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertLinkShortcut: IShortcutItem = {
|
||||
id: InsertHyperLinkToolbarOperation.id,
|
||||
binding: KeyCode.K | MetaKeys.CTRL_COMMAND,
|
||||
preconditions: whenSheetEditorFocused,
|
||||
};
|
||||
39
packages/sheets-hyper-link-ui/src/menu/schema.ts
Normal file
39
packages/sheets-hyper-link-ui/src/menu/schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { MenuSchemaType } from '@univerjs/ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { InsertHyperLinkToolbarOperation } from '../commands/operations/popup.operations';
|
||||
import { insertLinkMenuFactory, insertLinkMenuToolbarFactory } from './menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonInsertGroup.MEDIA]: {
|
||||
[InsertHyperLinkToolbarOperation.id]: {
|
||||
order: 1,
|
||||
gridLayout: { row: 1, column: 2, rowSpan: 2, showLabel: true },
|
||||
menuItemFactory: insertLinkMenuToolbarFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
order: 1,
|
||||
[InsertHyperLinkToolbarOperation.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: insertLinkMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
124
packages/sheets-hyper-link-ui/src/mobile-plugin.ts
Normal file
124
packages/sheets-hyper-link-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsHyperLinkUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Plugin,
|
||||
registerDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
|
||||
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { UniverMobileUIPlugin } from '@univerjs/ui';
|
||||
import pkg from '../package.json';
|
||||
import {
|
||||
defaultPluginConfig,
|
||||
SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY,
|
||||
} from './config/config';
|
||||
import { SheetsHyperLinkAutoFillController } from './controllers/auto-fill.controller';
|
||||
import { SheetsHyperLinkCopyPasteController } from './controllers/copy-paste.controller';
|
||||
import { SheetsHyperLinkPermissionController } from './controllers/hyper-link-permission.controller';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { SheetsHyperLinkMobilePopupController } from './controllers/mobile/popup.controller';
|
||||
import { SheetsHyperLinkRenderController } from './controllers/render-controllers/render.controller';
|
||||
import { SheetsHyperLinkUIController } from './controllers/ui.controller';
|
||||
import { SheetHyperLinkUrlController } from './controllers/url.controller';
|
||||
import { SheetsHyperLinkMobilePopupService } from './services/mobile-popup.service';
|
||||
import { ISheetsHyperLinkPopupService } from './services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from './services/resolver.service';
|
||||
import { SheetsHyperLinkSidePanelService } from './services/side-panel.service';
|
||||
import { SHEET_HYPER_LINK_UI_PLUGIN } from './types/const';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverFormulaEnginePlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverDocsUIPlugin,
|
||||
UniverSheetsHyperLinkPlugin,
|
||||
UniverSheetsMobileUIPlugin,
|
||||
UniverMobileUIPlugin
|
||||
)
|
||||
export class UniverSheetsHyperLinkMobileUIPlugin extends Plugin {
|
||||
static override pluginName: string = SHEET_HYPER_LINK_UI_PLUGIN;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsHyperLinkUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { menu, ...rest } = merge({}, defaultPluginConfig, this._config);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies = [
|
||||
[MobileComponentsController],
|
||||
[SheetsHyperLinkResolverService],
|
||||
[ISheetsHyperLinkPopupService, { useClass: SheetsHyperLinkMobilePopupService }],
|
||||
[SheetsHyperLinkSidePanelService],
|
||||
[SheetsHyperLinkMobilePopupController],
|
||||
[SheetsHyperLinkUIController],
|
||||
[SheetsHyperLinkAutoFillController],
|
||||
[SheetsHyperLinkCopyPasteController],
|
||||
[SheetsHyperLinkPermissionController],
|
||||
[SheetHyperLinkUrlController],
|
||||
[SheetsHyperLinkRenderController],
|
||||
] as Dependency[];
|
||||
|
||||
registerDependencies(this._injector, dependencies);
|
||||
this._injector.get(MobileComponentsController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
const renderManager = this._injector.get(IRenderManagerService);
|
||||
this.disposeWithMe(
|
||||
renderManager.registerRenderModule(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
[SheetsHyperLinkRenderController] as Dependency
|
||||
)
|
||||
);
|
||||
|
||||
this._injector.get(SheetsHyperLinkAutoFillController);
|
||||
this._injector.get(SheetsHyperLinkCopyPasteController);
|
||||
this._injector.get(SheetsHyperLinkUIController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(SheetsHyperLinkPermissionController);
|
||||
this._injector.get(SheetHyperLinkUrlController);
|
||||
this._injector.get(SheetsHyperLinkMobilePopupController);
|
||||
}
|
||||
}
|
||||
109
packages/sheets-hyper-link-ui/src/plugin.ts
Normal file
109
packages/sheets-hyper-link-ui/src/plugin.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsHyperLinkUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { SheetsHyperLinkAutoFillController } from './controllers/auto-fill.controller';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { SheetsHyperLinkCopyPasteController } from './controllers/copy-paste.controller';
|
||||
import { SheetsHyperLinkPermissionController } from './controllers/hyper-link-permission.controller';
|
||||
import { SheetsHyperLinkPopupController } from './controllers/popup.controller';
|
||||
import { SheetsHyperLinkRenderController } from './controllers/render-controllers/render.controller';
|
||||
import { SheetsHyperLinkUIController } from './controllers/ui.controller';
|
||||
import { SheetHyperLinkUrlController } from './controllers/url.controller';
|
||||
import { ISheetsHyperLinkPopupService, SheetsHyperLinkPopupService } from './services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from './services/resolver.service';
|
||||
import { SheetsHyperLinkSidePanelService } from './services/side-panel.service';
|
||||
import { SHEET_HYPER_LINK_UI_PLUGIN } from './types/const';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverFormulaEnginePlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverDocsUIPlugin,
|
||||
UniverSheetsHyperLinkPlugin,
|
||||
UniverSheetsUIPlugin
|
||||
)
|
||||
export class UniverSheetsHyperLinkUIPlugin extends Plugin {
|
||||
static override pluginName: string = SHEET_HYPER_LINK_UI_PLUGIN;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsHyperLinkUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
const dependencies: Dependency[] = [
|
||||
[SheetsHyperLinkResolverService],
|
||||
[ISheetsHyperLinkPopupService, { useClass: SheetsHyperLinkPopupService }],
|
||||
[SheetsHyperLinkSidePanelService],
|
||||
|
||||
[SheetsHyperLinkPopupController],
|
||||
[SheetsHyperLinkUIController],
|
||||
[SheetsHyperLinkAutoFillController],
|
||||
[SheetsHyperLinkCopyPasteController],
|
||||
[SheetsHyperLinkPermissionController],
|
||||
[SheetHyperLinkUrlController],
|
||||
|
||||
];
|
||||
dependencies.forEach((dep) => this._injector.add(dep));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
const renderManager = this._injector.get(IRenderManagerService);
|
||||
renderManager.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetsHyperLinkRenderController] as Dependency);
|
||||
|
||||
this._injector.get(SheetsHyperLinkAutoFillController);
|
||||
this._injector.get(SheetsHyperLinkCopyPasteController);
|
||||
this._injector.get(SheetsHyperLinkUIController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(SheetsHyperLinkPermissionController);
|
||||
this._injector.get(SheetHyperLinkUrlController);
|
||||
this._injector.get(SheetsHyperLinkPopupController);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* 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 { ContextService, CustomRangeType, IContextService, Injector, IUniverInstanceService } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { IEditorBridgeService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
import { of } from 'rxjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { HyperLinkEditSourceType } from '../../types/enums/edit-source';
|
||||
import { SheetsHyperLinkPopupService } from '../popup.service';
|
||||
|
||||
class TestDisposable {
|
||||
disposeCount = 0;
|
||||
canDisposeResult = true;
|
||||
|
||||
dispose(): void {
|
||||
this.disposeCount += 1;
|
||||
}
|
||||
|
||||
canDispose(): boolean {
|
||||
return this.canDisposeResult;
|
||||
}
|
||||
}
|
||||
|
||||
class TestSheetCanvasPopManagerService {
|
||||
readonly cellDisposable = new TestDisposable();
|
||||
readonly positionDisposable = new TestDisposable();
|
||||
readonly absoluteDisposable = new TestDisposable();
|
||||
readonly cellPopups: Array<{ row: number; col: number; popup: any; unitId?: string; subUnitId?: string }> = [];
|
||||
readonly positionPopups: Array<{ rect: any; popup: any; location: any }> = [];
|
||||
readonly absolutePopups: Array<{ rect: any; popup: any; unitId?: string; subUnitId?: string }> = [];
|
||||
|
||||
attachPopupToCell(row: number, col: number, popup: any, unitId?: string, subUnitId?: string): TestDisposable {
|
||||
this.cellPopups.push({ row, col, popup, unitId, subUnitId });
|
||||
return this.cellDisposable;
|
||||
}
|
||||
|
||||
attachPopupByPosition(rect: any, popup: any, location: any): TestDisposable {
|
||||
this.positionPopups.push({ rect, popup, location });
|
||||
return this.positionDisposable;
|
||||
}
|
||||
|
||||
attachPopupToAbsolutePosition(rect: any, popup: any, unitId?: string, subUnitId?: string): TestDisposable {
|
||||
this.absolutePopups.push({ rect, popup, unitId, subUnitId });
|
||||
return this.absoluteDisposable;
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
workbook: any = null;
|
||||
|
||||
getUnit(): any {
|
||||
return this.workbook;
|
||||
}
|
||||
}
|
||||
|
||||
class TestEditorBridgeService {
|
||||
visible = false;
|
||||
editCellState: any = null;
|
||||
|
||||
isVisible(): { visible: boolean } {
|
||||
return { visible: this.visible };
|
||||
}
|
||||
|
||||
getEditCellState(): any {
|
||||
return this.editCellState;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDocSelectionManagerService {
|
||||
activeTextRange: any = { startOffset: 1, endOffset: 3, collapsed: false };
|
||||
readonly replacedDocRanges: any[] = [];
|
||||
|
||||
getActiveTextRange(): any {
|
||||
return this.activeTextRange;
|
||||
}
|
||||
|
||||
replaceDocRanges(ranges: any, unitRange: any): void {
|
||||
this.replacedDocRanges.push({ ranges, unitRange });
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
render: any = null;
|
||||
|
||||
getRenderUnitById(): any {
|
||||
return this.render;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDialogService {
|
||||
open(): TestDisposable {
|
||||
return new TestDisposable();
|
||||
}
|
||||
|
||||
close(): void {}
|
||||
|
||||
closeAll(): void {}
|
||||
|
||||
getDialogs$() {
|
||||
return of([]);
|
||||
}
|
||||
}
|
||||
|
||||
function createService() {
|
||||
const injector = new Injector();
|
||||
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([SheetCanvasPopManagerService, { useClass: TestSheetCanvasPopManagerService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([IEditorBridgeService, { useClass: TestEditorBridgeService as never }]);
|
||||
injector.add([DocSelectionManagerService, { useClass: TestDocSelectionManagerService as never }]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([IDialogService, { useClass: TestDialogService }]);
|
||||
injector.add([SheetsHyperLinkPopupService]);
|
||||
|
||||
return {
|
||||
service: injector.get(SheetsHyperLinkPopupService),
|
||||
sheetCanvasPopManagerService: injector.get(SheetCanvasPopManagerService) as unknown as TestSheetCanvasPopManagerService,
|
||||
editorBridgeService: injector.get(IEditorBridgeService) as unknown as TestEditorBridgeService,
|
||||
textSelectionManagerService: injector.get(DocSelectionManagerService) as unknown as TestDocSelectionManagerService,
|
||||
univerInstanceService: injector.get(IUniverInstanceService) as unknown as TestUniverInstanceService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SheetsHyperLinkPopupService', () => {
|
||||
it('shows and hides cell popups without recreating the same viewing popup', () => {
|
||||
const { service, sheetCanvasPopManagerService } = createService();
|
||||
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 2,
|
||||
showAll: true,
|
||||
editPermission: true,
|
||||
copyPermission: true,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
|
||||
expect(sheetCanvasPopManagerService.cellPopups).toHaveLength(1);
|
||||
expect(service.currentPopup).toEqual(expect.objectContaining({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 2,
|
||||
showAll: true,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
}));
|
||||
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 2,
|
||||
showAll: true,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
|
||||
expect(sheetCanvasPopManagerService.cellPopups).toHaveLength(1);
|
||||
|
||||
service.hideCurrentPopup();
|
||||
expect(sheetCanvasPopManagerService.cellDisposable.disposeCount).toBe(1);
|
||||
expect(service.currentPopup).toBeNull();
|
||||
});
|
||||
|
||||
it('routes popups by source type and popup callbacks close the active popup', () => {
|
||||
const { service, sheetCanvasPopManagerService } = createService();
|
||||
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
col: 0,
|
||||
customRange: { rangeId: 'r1', startIndex: 1, endIndex: 3, rangeType: CustomRangeType.HYPERLINK },
|
||||
customRangeRect: { left: 0, top: 0, right: 10, bottom: 10 },
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
expect(sheetCanvasPopManagerService.absolutePopups).toHaveLength(1);
|
||||
|
||||
sheetCanvasPopManagerService.absolutePopups[0].popup.onClickOutside();
|
||||
expect(sheetCanvasPopManagerService.absoluteDisposable.disposeCount).toBe(1);
|
||||
expect(service.currentPopup).toBeNull();
|
||||
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
col: 0,
|
||||
customRange: { rangeId: 'r2', startIndex: 2, endIndex: 4, rangeType: CustomRangeType.HYPERLINK },
|
||||
customRangeRect: { left: 1, top: 1, right: 9, bottom: 9 },
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
|
||||
expect(sheetCanvasPopManagerService.positionPopups).toHaveLength(1);
|
||||
sheetCanvasPopManagerService.positionPopups[0].popup.onClick();
|
||||
expect(sheetCanvasPopManagerService.positionDisposable.disposeCount).toBe(1);
|
||||
expect(service.currentPopup).toBeNull();
|
||||
});
|
||||
|
||||
it('does not open incomplete popups and replaces an older visible popup', () => {
|
||||
const { service, sheetCanvasPopManagerService } = createService();
|
||||
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
col: 0,
|
||||
customRangeRect: { left: 0, top: 0, right: 10, bottom: 10 },
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
col: 0,
|
||||
customRangeRect: { left: 0, top: 0, right: 10, bottom: 10 },
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
expect(sheetCanvasPopManagerService.absolutePopups).toHaveLength(0);
|
||||
expect(sheetCanvasPopManagerService.positionPopups).toHaveLength(0);
|
||||
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 0,
|
||||
col: 0,
|
||||
showAll: true,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
showAll: true,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
|
||||
expect(sheetCanvasPopManagerService.cellPopups).toHaveLength(2);
|
||||
expect(sheetCanvasPopManagerService.cellDisposable.disposeCount).toBe(1);
|
||||
expect(service.currentPopup).toEqual(expect.objectContaining({ row: 2, col: 3 }));
|
||||
});
|
||||
|
||||
it('starts viewing edits from cell content and preserves editor state while keep-visible is enabled', () => {
|
||||
const { service, sheetCanvasPopManagerService, univerInstanceService } = createService();
|
||||
univerInstanceService.workbook = {
|
||||
getSheetBySheetId: () => ({
|
||||
getCellRaw: () => ({ v: 'Cell Value' }),
|
||||
}),
|
||||
};
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
expect(sheetCanvasPopManagerService.cellPopups).toHaveLength(1);
|
||||
expect(service.currentEditing).toEqual(expect.objectContaining({
|
||||
label: 'Cell Value',
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
}));
|
||||
|
||||
service.setIsKeepVisible(true);
|
||||
service.endEditing();
|
||||
expect(service.currentEditing).toEqual(expect.objectContaining({ type: HyperLinkEditSourceType.VIEWING }));
|
||||
|
||||
service.setIsKeepVisible(false);
|
||||
service.endEditing(HyperLinkEditSourceType.VIEWING);
|
||||
expect(service.currentEditing).toBeNull();
|
||||
});
|
||||
|
||||
it('uses rich text cell content as the default label when starting a viewing edit', () => {
|
||||
const { service, univerInstanceService } = createService();
|
||||
univerInstanceService.workbook = {
|
||||
getSheetBySheetId: () => ({
|
||||
getCellRaw: () => ({ p: { body: { dataStream: 'Rich Text\r\n' } } }),
|
||||
}),
|
||||
};
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
|
||||
expect(service.currentEditing).toEqual(expect.objectContaining({ label: 'Rich Text' }));
|
||||
});
|
||||
|
||||
it('keeps a popup hidden when the same link is already being edited', () => {
|
||||
const { service, sheetCanvasPopManagerService } = createService();
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
service.showPopup({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
showAll: true,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
|
||||
expect(sheetCanvasPopManagerService.cellPopups).toHaveLength(1);
|
||||
expect(service.currentPopup).toBeNull();
|
||||
});
|
||||
|
||||
it('does not start document-editing flow when editor state has no document body', () => {
|
||||
const { service, editorBridgeService, textSelectionManagerService } = createService();
|
||||
editorBridgeService.visible = true;
|
||||
editorBridgeService.editCellState = {
|
||||
documentLayoutObject: {
|
||||
documentModel: {
|
||||
getBody: () => null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
|
||||
expect(textSelectionManagerService.replacedDocRanges).toHaveLength(0);
|
||||
expect(service.currentEditing).toBeNull();
|
||||
});
|
||||
|
||||
it('does not start document-editing flow when the editor is hidden', () => {
|
||||
const { service, textSelectionManagerService } = createService();
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
|
||||
expect(textSelectionManagerService.replacedDocRanges).toHaveLength(0);
|
||||
expect(service.currentEditing).toBeNull();
|
||||
});
|
||||
|
||||
it('selects the full editor text when adding a link from a collapsed editing selection', () => {
|
||||
const { service, editorBridgeService, textSelectionManagerService, sheetCanvasPopManagerService } = createService();
|
||||
editorBridgeService.visible = true;
|
||||
editorBridgeService.editCellState = {
|
||||
documentLayoutObject: {
|
||||
documentModel: {
|
||||
getBody: () => ({ dataStream: 'Full Label\r\n', customRanges: [] }),
|
||||
},
|
||||
},
|
||||
};
|
||||
textSelectionManagerService.activeTextRange = { startOffset: 2, endOffset: 2, collapsed: true };
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
|
||||
expect(textSelectionManagerService.replacedDocRanges[0].ranges).toEqual([{
|
||||
startOffset: 0,
|
||||
endOffset: 10,
|
||||
collapsed: false,
|
||||
label: 'Full Label',
|
||||
}]);
|
||||
expect(sheetCanvasPopManagerService.absolutePopups).toHaveLength(0);
|
||||
expect(service.currentEditing).toBeNull();
|
||||
});
|
||||
|
||||
it('expands an editor selection to the full overlapping hyperlink range', () => {
|
||||
const { service, editorBridgeService, textSelectionManagerService } = createService();
|
||||
editorBridgeService.visible = true;
|
||||
editorBridgeService.editCellState = {
|
||||
documentLayoutObject: {
|
||||
documentModel: {
|
||||
getBody: () => ({
|
||||
dataStream: 'abcdef\r\n',
|
||||
customRanges: [{ rangeId: 'link-1', startIndex: 1, endIndex: 4, rangeType: CustomRangeType.HYPERLINK }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
textSelectionManagerService.activeTextRange = { startOffset: 2, endOffset: 3, collapsed: false };
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.EDITING,
|
||||
});
|
||||
|
||||
expect(textSelectionManagerService.replacedDocRanges[0].ranges).toEqual([{
|
||||
startOffset: 1,
|
||||
endOffset: 5,
|
||||
collapsed: false,
|
||||
label: 'bcde',
|
||||
}]);
|
||||
expect(service.currentEditing).toBeNull();
|
||||
});
|
||||
|
||||
it('ends viewing edit sessions from edit popup click handlers', () => {
|
||||
const { service, sheetCanvasPopManagerService } = createService();
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
sheetCanvasPopManagerService.cellPopups[0].popup.onClickOutside();
|
||||
expect(service.currentEditing).toBeNull();
|
||||
|
||||
service.startAddEditing({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 1,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
sheetCanvasPopManagerService.cellPopups[1].popup.onContextMenu();
|
||||
expect(service.currentEditing).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* 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 { IMessageProps } from '@univerjs/design';
|
||||
import { ICommandService, IConfigService, Injector, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import { SetSelectionsOperation, SetWorksheetActiveOperation } from '@univerjs/sheets';
|
||||
import { ERROR_RANGE, SheetHyperLinkType } from '@univerjs/sheets-hyper-link';
|
||||
import { ScrollToRangeOperation } from '@univerjs/sheets-ui';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { SheetsHyperLinkResolverService } from '../resolver.service';
|
||||
|
||||
class TestCommandService {
|
||||
readonly commands: Array<{ id: string; params: any }> = [];
|
||||
activateResult = true;
|
||||
|
||||
async executeCommand(id: string, params: any): Promise<boolean> {
|
||||
this.commands.push({ id, params });
|
||||
return id === SetWorksheetActiveOperation.id ? this.activateResult : true;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDefinedNamesService {
|
||||
namedRanges = new Map<string, { formulaOrRefString: string }>();
|
||||
worksheetsByRef: any[] = [];
|
||||
readonly focusedRanges: Array<{ unitId: string; rangeId: string }> = [];
|
||||
|
||||
getValueById(_unitId: string, rangeId: string): { formulaOrRefString: string } | undefined {
|
||||
return this.namedRanges.get(rangeId);
|
||||
}
|
||||
|
||||
getWorksheetByRef(): any {
|
||||
return this.worksheetsByRef.shift();
|
||||
}
|
||||
|
||||
focusRange(unitId: string, rangeId: string): void {
|
||||
this.focusedRanges.push({ unitId, rangeId });
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageService {
|
||||
readonly messages: IMessageProps[] = [];
|
||||
|
||||
show(options: IMessageProps): void {
|
||||
this.messages.push(options);
|
||||
}
|
||||
}
|
||||
|
||||
class TestLocaleService {
|
||||
t(key: string): string {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
class TestConfigService {
|
||||
configs: any[] = [];
|
||||
|
||||
getConfig(): any {
|
||||
return this.configs.length ? this.configs.shift() : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
currentUnit: any = null;
|
||||
unit: any = null;
|
||||
|
||||
getCurrentUnitOfType(): any {
|
||||
return this.currentUnit;
|
||||
}
|
||||
|
||||
getUnit(): any {
|
||||
return this.unit ?? this.currentUnit;
|
||||
}
|
||||
}
|
||||
|
||||
function createWorksheet(sheetId: string, options?: { hidden?: boolean }) {
|
||||
return {
|
||||
getSheetId: () => sheetId,
|
||||
getMergeData: () => [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2 }],
|
||||
getMaxColumns: () => 10,
|
||||
getMaxRows: () => 10,
|
||||
isSheetHidden: () => !!options?.hidden,
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkbook(options?: {
|
||||
activeSheet?: any;
|
||||
targetSheet?: any;
|
||||
hiddenSheets?: string[];
|
||||
}) {
|
||||
return {
|
||||
getUnitId: () => 'unit-1',
|
||||
getActiveSheet: () => options?.activeSheet ?? createWorksheet('sheet-1'),
|
||||
getSheetBySheetId: (sheetId: string) => (options?.targetSheet?.getSheetId() === sheetId ? options.targetSheet : null),
|
||||
getHiddenWorksheets: () => options?.hiddenSheets ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function createResolver() {
|
||||
const injector = new Injector();
|
||||
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
injector.add([IDefinedNamesService, { useClass: TestDefinedNamesService as never }]);
|
||||
injector.add([IMessageService, { useClass: TestMessageService as never }]);
|
||||
injector.add([LocaleService, { useClass: TestLocaleService as never }]);
|
||||
injector.add([IConfigService, { useClass: TestConfigService as never }]);
|
||||
injector.add([SheetsHyperLinkResolverService]);
|
||||
|
||||
return {
|
||||
resolver: injector.get(SheetsHyperLinkResolverService),
|
||||
commandService: injector.get(ICommandService) as unknown as TestCommandService,
|
||||
definedNamesService: injector.get(IDefinedNamesService) as unknown as TestDefinedNamesService,
|
||||
messageService: injector.get(IMessageService) as unknown as TestMessageService,
|
||||
configService: injector.get(IConfigService) as unknown as TestConfigService,
|
||||
univerInstanceService: injector.get(IUniverInstanceService) as unknown as TestUniverInstanceService,
|
||||
};
|
||||
}
|
||||
|
||||
const originalOpen = window.open;
|
||||
const waitForNavigationJobs = () => new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
describe('SheetsHyperLinkResolverService', () => {
|
||||
afterEach(() => {
|
||||
window.open = originalOpen;
|
||||
});
|
||||
|
||||
it('should navigate to sheet ranges and execute selection and scroll commands', async () => {
|
||||
const { resolver, commandService, univerInstanceService } = createResolver();
|
||||
const targetSheet = createWorksheet('sheet-2');
|
||||
univerInstanceService.currentUnit = createWorkbook({ targetSheet });
|
||||
|
||||
await resolver.navigateToRange('unit-1', 'sheet-2', { startRow: 1, endRow: 20, startColumn: 1, endColumn: 20 }, true);
|
||||
|
||||
expect(commandService.commands).toEqual([
|
||||
{ id: SetWorksheetActiveOperation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-2' } },
|
||||
{
|
||||
id: SetSelectionsOperation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-2',
|
||||
selections: [{ range: { startRow: 1, endRow: 9, startColumn: 1, endColumn: 9 }, primary: null }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ScrollToRangeOperation.id,
|
||||
params: { range: { startRow: 1, endRow: 9, startColumn: 1, endColumn: 9 }, forceTop: true },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should show user-facing errors when target sheets or defined names are invalid', async () => {
|
||||
const { resolver, definedNamesService, messageService, univerInstanceService } = createResolver();
|
||||
univerInstanceService.currentUnit = createWorkbook({ hiddenSheets: ['sheet-hidden'] });
|
||||
definedNamesService.namedRanges.set('named-range', { formulaOrRefString: 'Sheet1!A1' });
|
||||
definedNamesService.worksheetsByRef.push(null, createWorksheet('sheet-hidden', { hidden: true }), createWorksheet('sheet-1'));
|
||||
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { rangeid: 'missing-range' } } as any);
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { rangeid: 'named-range' } } as any);
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { rangeid: 'named-range' } } as any);
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { rangeid: 'named-range' } } as any);
|
||||
|
||||
await resolver.navigateToSheetById('unit-1', 'sheet-missing');
|
||||
await resolver.navigateToSheetById('unit-1', 'sheet-hidden');
|
||||
|
||||
expect(messageService.messages).toEqual([
|
||||
{ content: 'sheets-hyper-link-ui.message.refError', type: 'error' },
|
||||
{ content: 'sheets-hyper-link-ui.message.hiddenSheet', type: 'error' },
|
||||
{ content: 'sheets-hyper-link-ui.message.noSheet', type: 'error' },
|
||||
{ content: 'sheets-hyper-link-ui.message.noSheet', type: 'error' },
|
||||
]);
|
||||
expect(definedNamesService.focusedRanges).toEqual([{ unitId: 'unit-1', rangeId: 'named-range' }]);
|
||||
});
|
||||
|
||||
it('should open external links via configured handlers or fallback window.open', async () => {
|
||||
const { resolver, configService } = createResolver();
|
||||
const navigatedUrls: string[] = [];
|
||||
const openedUrls: any[] = [];
|
||||
window.open = ((...args: any[]) => {
|
||||
openedUrls.push(args);
|
||||
return null;
|
||||
}) as typeof window.open;
|
||||
configService.configs.push(
|
||||
{ urlHandler: { navigateToOtherWebsite: (url: string) => navigatedUrls.push(url) } },
|
||||
undefined
|
||||
);
|
||||
|
||||
await resolver.navigateToOtherWebsite('https://univer.ai');
|
||||
await resolver.navigateToOtherWebsite('https://openai.com');
|
||||
await resolver.navigateToOtherWebsite('javascript:alert(1)');
|
||||
resolver.navigate({ type: SheetHyperLinkType.URL, url: 'https://example.com' } as any);
|
||||
|
||||
expect(navigatedUrls).toEqual(['https://univer.ai']);
|
||||
expect(openedUrls).toEqual([
|
||||
['https://openai.com', '_blank', 'noopener noreferrer'],
|
||||
['https://example.com', '_blank', 'noopener noreferrer'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should interpret workbook-style links through the public navigate entry', async () => {
|
||||
const { resolver, commandService, univerInstanceService } = createResolver();
|
||||
univerInstanceService.currentUnit = createWorkbook({ targetSheet: createWorksheet('sheet-2') });
|
||||
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { gid: 'sheet-2', range: 'B2:C3' } } as any);
|
||||
await waitForNavigationJobs();
|
||||
|
||||
expect(commandService.commands.map((command) => command.id)).toEqual([
|
||||
SetWorksheetActiveOperation.id,
|
||||
SetSelectionsOperation.id,
|
||||
ScrollToRangeOperation.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore invalid workbook targets without executing navigation commands', async () => {
|
||||
const { resolver, commandService, univerInstanceService } = createResolver();
|
||||
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { gid: 'sheet-1' } } as any);
|
||||
await resolver.navigateToSheetById('unit-1', 'sheet-1');
|
||||
|
||||
univerInstanceService.currentUnit = createWorkbook({ activeSheet: null });
|
||||
await resolver.navigateToSheetById('unit-1', 'sheet-1');
|
||||
|
||||
univerInstanceService.currentUnit = createWorkbook({ targetSheet: createWorksheet('sheet-2') });
|
||||
commandService.activateResult = false;
|
||||
await resolver.navigateToSheetById('unit-1', 'sheet-2');
|
||||
|
||||
resolver.navigate({ type: SheetHyperLinkType.SHEET, searchObj: { gid: 'sheet-2', range: ERROR_RANGE } } as any);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(commandService.commands).toEqual([
|
||||
{ id: SetWorksheetActiveOperation.id, params: { unitId: 'unit-1', subUnitId: 'sheet-2' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stay on the current worksheet when the hyperlink already points there', async () => {
|
||||
const { resolver, commandService, univerInstanceService } = createResolver();
|
||||
const activeSheet = createWorksheet('sheet-1');
|
||||
univerInstanceService.currentUnit = createWorkbook({ activeSheet });
|
||||
|
||||
const result = await resolver.navigateToSheetById('unit-1', 'sheet-1');
|
||||
|
||||
expect(result).toBe(activeSheet);
|
||||
expect(commandService.commands).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 { ISheetHyperLink } from '@univerjs/sheets-hyper-link';
|
||||
import type { ICustomHyperLinkView } from '../side-panel.service';
|
||||
import { Injector } from '@univerjs/core';
|
||||
import { SheetHyperLinkType } from '@univerjs/sheets-hyper-link';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SheetsHyperLinkSidePanelService } from '../side-panel.service';
|
||||
|
||||
function createService(): SheetsHyperLinkSidePanelService {
|
||||
const injector = new Injector();
|
||||
injector.add([SheetsHyperLinkSidePanelService]);
|
||||
return injector.get(SheetsHyperLinkSidePanelService);
|
||||
}
|
||||
|
||||
describe('SheetsHyperLinkSidePanelService', () => {
|
||||
it('should register, resolve and remove custom hyperlink views', () => {
|
||||
const service = createService();
|
||||
const customView: ICustomHyperLinkView = {
|
||||
type: 'sheet-anchor',
|
||||
option: { label: 'Sheet Anchor', value: 'sheet-anchor' },
|
||||
Form: () => null,
|
||||
convert: () => ({ display: 'A1', payload: '#A1', type: 'sheet-anchor' }),
|
||||
match: (link) => link.payload === '#A1',
|
||||
};
|
||||
|
||||
expect(service.isBuiltInLinkType(SheetHyperLinkType.URL)).toBe(false);
|
||||
expect(service.isBuiltInLinkType('custom')).toBe(true);
|
||||
|
||||
service.registerCustomHyperLink(customView);
|
||||
expect(service.getOptions()).toEqual([customView.option]);
|
||||
expect(service.getCustomHyperLink('sheet-anchor')).toBe(customView);
|
||||
expect(service.findCustomHyperLink({ payload: '#A1' } as ISheetHyperLink)).toBe(customView);
|
||||
|
||||
service.removeCustomHyperLink('sheet-anchor');
|
||||
expect(service.getCustomHyperLink('sheet-anchor')).toBeUndefined();
|
||||
|
||||
service.registerCustomHyperLink(customView);
|
||||
service.dispose();
|
||||
expect(service.getOptions()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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 { ICustomRange, INeedCheckDisposable, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IHyperLinkEditing, IHyperLinkPopupOptions } from './popup.service';
|
||||
import { BuildTextUtils, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType } from '@univerjs/core';
|
||||
import { getCustomRangePosition, getEditingCustomRangePosition } from '@univerjs/sheets-ui';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
import { MobileCellLinkEdit } from '../views/MobileCellLinkEdit';
|
||||
import { MobileCellLinkPopup } from '../views/MobileCellLinkPopup';
|
||||
import { SheetsHyperLinkPopupService } from './popup.service';
|
||||
|
||||
const MOBILE_HYPER_LINK_EDITOR_DIALOG_ID = 'sheet-mobile-hyper-link-editor';
|
||||
const MOBILE_HYPER_LINK_VIEWER_DIALOG_ID = 'sheet-mobile-hyper-link-viewer';
|
||||
|
||||
export class SheetsHyperLinkMobilePopupService extends SheetsHyperLinkPopupService {
|
||||
override showPopup(location: IHyperLinkPopupOptions): void {
|
||||
if (this.getIsKeepVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._currentPopup && this._isEqualLink(location, this._currentPopup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideCurrentPopup(undefined, true);
|
||||
const currentEditing = this._currentEditing$.getValue();
|
||||
if (currentEditing && this._isEqualLink(location, currentEditing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, row, col, customRange } = location;
|
||||
if (!location.showAll && !customRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disposable: INeedCheckDisposable = {
|
||||
canDispose: () => true,
|
||||
dispose: () => this._dialogService.close(MOBILE_HYPER_LINK_VIEWER_DIALOG_ID),
|
||||
};
|
||||
this._currentPopup = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
disposable,
|
||||
row,
|
||||
col,
|
||||
editPermission: !!location.editPermission,
|
||||
copyPermission: !!location.copyPermission,
|
||||
customRange,
|
||||
type: location.type,
|
||||
showAll: location.showAll,
|
||||
};
|
||||
this._currentPopup$.next(this._currentPopup);
|
||||
this._dialogService.open({
|
||||
id: MOBILE_HYPER_LINK_VIEWER_DIALOG_ID,
|
||||
title: { title: 'sheets-hyper-link-ui.popup.title' },
|
||||
children: { label: MobileCellLinkPopup.componentKey },
|
||||
onClose: () => this.hideCurrentPopup(undefined, true),
|
||||
});
|
||||
}
|
||||
|
||||
override startAddEditing(link: IHyperLinkEditing): void {
|
||||
const { unitId, subUnitId, type } = link;
|
||||
let label: string;
|
||||
if (type === HyperLinkEditSourceType.EDITING) {
|
||||
const range = this._getEditingRange();
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._textSelectionManagerService.replaceDocRanges([{ ...range }], {
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
label = range.label;
|
||||
} else {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
const cell = worksheet?.getCellRaw(link.row, link.col);
|
||||
label = cell?.p
|
||||
? BuildTextUtils.transform.getPlainText(cell.p.body?.dataStream ?? '')
|
||||
: (cell?.v ?? '').toString();
|
||||
}
|
||||
|
||||
this._openMobileEditor({ ...link, label });
|
||||
}
|
||||
|
||||
override startEditing(link: Required<IHyperLinkEditing>): void {
|
||||
this._currentEditingPopup?.dispose();
|
||||
this.hideCurrentPopup(undefined, true);
|
||||
|
||||
let customRange: ICustomRange;
|
||||
let label: string;
|
||||
if (link.type === HyperLinkEditSourceType.EDITING) {
|
||||
const customRangeInfo = getEditingCustomRangePosition(
|
||||
this._injector,
|
||||
link.unitId,
|
||||
link.subUnitId,
|
||||
link.row,
|
||||
link.col,
|
||||
link.customRangeId
|
||||
);
|
||||
if (!customRangeInfo || !customRangeInfo.rects?.length) {
|
||||
return;
|
||||
}
|
||||
customRange = customRangeInfo.customRange;
|
||||
label = customRangeInfo.label;
|
||||
this._textSelectionManagerService.replaceDocRanges([{
|
||||
startOffset: customRange.startIndex,
|
||||
endOffset: customRange.endIndex + 1,
|
||||
}]);
|
||||
} else {
|
||||
const customRangeInfo = getCustomRangePosition(
|
||||
this._injector,
|
||||
link.unitId,
|
||||
link.subUnitId,
|
||||
link.row,
|
||||
link.col,
|
||||
link.customRangeId
|
||||
);
|
||||
if (!customRangeInfo || !customRangeInfo.rects?.length) {
|
||||
return;
|
||||
}
|
||||
customRange = customRangeInfo.customRange;
|
||||
label = customRangeInfo.label;
|
||||
}
|
||||
|
||||
this._openMobileEditor({ ...link, customRange, label });
|
||||
}
|
||||
|
||||
override endEditing(type?: HyperLinkEditSourceType): void {
|
||||
if (this.getIsKeepVisible()) {
|
||||
return;
|
||||
}
|
||||
const current = this._currentEditing$.getValue();
|
||||
if (current && (!type || type === current.type)) {
|
||||
this._currentEditingPopup?.dispose();
|
||||
this._dialogService.close(MOBILE_HYPER_LINK_EDITOR_DIALOG_ID);
|
||||
this._currentEditing$.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
private _openMobileEditor(editing: IHyperLinkEditing & { customRange?: ICustomRange; label?: string }): void {
|
||||
this._currentEditing$.next(editing);
|
||||
this._dialogService.open({
|
||||
id: MOBILE_HYPER_LINK_EDITOR_DIALOG_ID,
|
||||
title: { title: 'sheets-hyper-link-ui.form.addTitle' },
|
||||
children: { label: MobileCellLinkEdit.componentKey },
|
||||
maskClosable: false,
|
||||
onClose: () => this.endEditing(editing.type),
|
||||
});
|
||||
}
|
||||
|
||||
private _isEqualLink(
|
||||
location: IHyperLinkPopupOptions,
|
||||
current: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
row: number;
|
||||
col: number;
|
||||
customRange?: Nullable<ICustomRange>;
|
||||
type: HyperLinkEditSourceType;
|
||||
} | null
|
||||
): boolean {
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return location.unitId === current.unitId
|
||||
&& location.subUnitId === current.subUnitId
|
||||
&& location.row === current.row
|
||||
&& location.col === current.col
|
||||
&& location.customRange?.rangeId === current.customRange?.rangeId
|
||||
&& location.type === current.type;
|
||||
}
|
||||
}
|
||||
411
packages/sheets-hyper-link-ui/src/services/popup.service.ts
Normal file
411
packages/sheets-hyper-link-ui/src/services/popup.service.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 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 { ICustomRange, IDisposable, INeedCheckDisposable, ITextRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IBoundRectNoAngle } from '@univerjs/engine-render';
|
||||
import type { ISheetLocationBase } from '@univerjs/sheets';
|
||||
import type { ICanvasPopup } from '@univerjs/sheets-ui';
|
||||
import type { Observable } from 'rxjs';
|
||||
import {
|
||||
BuildTextUtils,
|
||||
createIdentifier,
|
||||
CustomRangeType,
|
||||
Disposable,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { calcDocRangePositions } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { getCustomRangePosition, getEditingCustomRangePosition, IEditorBridgeService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
import { CellLinkEdit } from '../views/CellLinkEdit';
|
||||
import { CellLinkPopup } from '../views/CellLinkPopup';
|
||||
|
||||
export interface IHyperLinkPopup {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
disposable: INeedCheckDisposable;
|
||||
row: number;
|
||||
col: number;
|
||||
editPermission?: boolean;
|
||||
copyPermission?: boolean;
|
||||
customRange?: Nullable<ICustomRange>;
|
||||
type: HyperLinkEditSourceType;
|
||||
showAll?: boolean;
|
||||
}
|
||||
|
||||
export interface IHyperLinkEditing {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
row: number;
|
||||
col: number;
|
||||
customRangeId?: string;
|
||||
type: HyperLinkEditSourceType;
|
||||
}
|
||||
|
||||
export interface ISheetsHyperLinkPopupService {
|
||||
readonly currentPopup: IHyperLinkPopup | null;
|
||||
readonly currentPopup$: Observable<IHyperLinkPopup | null>;
|
||||
readonly currentEditing: (IHyperLinkEditing & { customRange?: ICustomRange; label?: string }) | null;
|
||||
readonly currentEditing$: Observable<(IHyperLinkEditing & { customRange?: ICustomRange; label?: string }) | null>;
|
||||
setIsKeepVisible(value: boolean): void;
|
||||
getIsKeepVisible(): boolean;
|
||||
showPopup(location: IHyperLinkPopupOptions): void;
|
||||
hideCurrentPopup(type?: HyperLinkEditSourceType, force?: boolean): void;
|
||||
startAddEditing(link: IHyperLinkEditing): void;
|
||||
startEditing(link: Required<IHyperLinkEditing>): void;
|
||||
endEditing(type?: HyperLinkEditSourceType): void;
|
||||
}
|
||||
|
||||
export const ISheetsHyperLinkPopupService = createIdentifier<ISheetsHyperLinkPopupService>('sheets-hyper-link-ui.popup.service');
|
||||
|
||||
const isEqualLink = (a: IHyperLinkPopupOptions, b: Omit<IHyperLinkPopup, 'disposable' | 'editPermission'>) => {
|
||||
return (
|
||||
a.unitId === b.unitId
|
||||
&& a.subUnitId === b.subUnitId
|
||||
&& a.row === b.row
|
||||
&& a.col === b.col
|
||||
&& a.customRange?.rangeId === b.customRange?.rangeId
|
||||
&& a.type === b.type
|
||||
);
|
||||
};
|
||||
|
||||
export interface IHyperLinkPopupOptions extends ISheetLocationBase {
|
||||
editPermission?: boolean;
|
||||
copyPermission?: boolean;
|
||||
customRange?: Nullable<ICustomRange>;
|
||||
customRangeRect?: Nullable<IBoundRectNoAngle>;
|
||||
showAll?: boolean;
|
||||
type: HyperLinkEditSourceType;
|
||||
}
|
||||
|
||||
export class SheetsHyperLinkPopupService extends Disposable implements ISheetsHyperLinkPopupService {
|
||||
protected _currentPopup: IHyperLinkPopup | null = null;
|
||||
protected _currentPopup$ = new Subject<IHyperLinkPopup | null>();
|
||||
currentPopup$ = this._currentPopup$.asObservable();
|
||||
protected _currentEditingPopup: Nullable<IDisposable> = null;
|
||||
|
||||
protected _currentEditing$ = new BehaviorSubject<(IHyperLinkEditing & { customRange?: ICustomRange; label?: string }) | null>(null);
|
||||
currentEditing$ = this._currentEditing$.asObservable();
|
||||
|
||||
private _isKeepVisible: boolean = false;
|
||||
|
||||
get currentPopup() {
|
||||
return this._currentPopup;
|
||||
}
|
||||
|
||||
get currentEditing() {
|
||||
return this._currentEditing$.getValue();
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(SheetCanvasPopManagerService) protected readonly _sheetCanvasPopManagerService: SheetCanvasPopManagerService,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IUniverInstanceService protected readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
|
||||
@Inject(DocSelectionManagerService) protected readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@IRenderManagerService protected readonly _renderManagerService: IRenderManagerService,
|
||||
@IDialogService protected readonly _dialogService: IDialogService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(() => {
|
||||
this.hideCurrentPopup();
|
||||
this.endEditing();
|
||||
|
||||
this._currentEditing$.complete();
|
||||
this._currentPopup$.complete();
|
||||
});
|
||||
}
|
||||
|
||||
public setIsKeepVisible(v: boolean) {
|
||||
this._isKeepVisible = v;
|
||||
}
|
||||
|
||||
public getIsKeepVisible() {
|
||||
return this._isKeepVisible;
|
||||
}
|
||||
|
||||
showPopup(location: IHyperLinkPopupOptions) {
|
||||
if (this._currentPopup && isEqualLink(location, this._currentPopup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideCurrentPopup(undefined, true);
|
||||
const currentEditing = this._currentEditing$.getValue();
|
||||
if (currentEditing && isEqualLink(location, currentEditing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, row, col, customRangeRect, customRange } = location;
|
||||
let disposable: Nullable<INeedCheckDisposable>;
|
||||
const popup: ICanvasPopup = {
|
||||
componentKey: CellLinkPopup.componentKey,
|
||||
direction: 'bottom',
|
||||
onClickOutside: () => {
|
||||
this.hideCurrentPopup();
|
||||
},
|
||||
onClick: () => {
|
||||
this.hideCurrentPopup(location.type, true);
|
||||
},
|
||||
};
|
||||
if (location.type === HyperLinkEditSourceType.EDITING) {
|
||||
if (!customRange) {
|
||||
return;
|
||||
}
|
||||
disposable = customRangeRect && this._sheetCanvasPopManagerService.attachPopupToAbsolutePosition(
|
||||
customRangeRect,
|
||||
popup
|
||||
);
|
||||
} else {
|
||||
if (location.showAll) {
|
||||
disposable = this._sheetCanvasPopManagerService.attachPopupToCell(location.row, location.col, popup, unitId, subUnitId);
|
||||
} else {
|
||||
if (!customRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposable = customRangeRect && this._sheetCanvasPopManagerService.attachPopupByPosition(
|
||||
customRangeRect,
|
||||
popup,
|
||||
location
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (disposable) {
|
||||
if (this._currentPopup) {
|
||||
this._currentPopup.disposable?.dispose();
|
||||
}
|
||||
this._currentPopup = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
disposable,
|
||||
row,
|
||||
col,
|
||||
editPermission: !!location.editPermission,
|
||||
copyPermission: !!location.copyPermission,
|
||||
customRange,
|
||||
type: location.type,
|
||||
showAll: location.showAll,
|
||||
};
|
||||
this._currentPopup$.next(this._currentPopup);
|
||||
}
|
||||
}
|
||||
|
||||
hideCurrentPopup(type?: HyperLinkEditSourceType, force?: boolean) {
|
||||
if (!this._currentPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (((!type || type === this._currentPopup.type) && this._currentPopup.disposable.canDispose()) || force) {
|
||||
this._currentPopup?.disposable?.dispose();
|
||||
this._currentPopup = null;
|
||||
this._currentPopup$.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.hideCurrentPopup();
|
||||
this.endEditing();
|
||||
this._currentPopup$.complete();
|
||||
this._currentEditing$.complete();
|
||||
}
|
||||
|
||||
protected _getEditingRange(): Nullable<ITextRange & { label: string }> {
|
||||
const visible = this._editorBridgeService.isVisible().visible;
|
||||
const state = this._editorBridgeService.getEditCellState();
|
||||
if (visible && state) {
|
||||
const textRange = this._textSelectionManagerService.getActiveTextRange();
|
||||
const body = state.documentLayoutObject.documentModel?.getBody();
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
if (!textRange || textRange.collapsed) {
|
||||
return {
|
||||
startOffset: 0,
|
||||
endOffset: body.dataStream.length - 2,
|
||||
collapsed: body.dataStream.length - 2 === 0,
|
||||
label: BuildTextUtils.transform.getPlainText(body.dataStream),
|
||||
};
|
||||
}
|
||||
const links = BuildTextUtils.customRange.getCustomRangesInterestsWithSelection(textRange, body.customRanges?.filter((i) => i.rangeType === CustomRangeType.HYPERLINK) ?? []);
|
||||
let start = textRange.startOffset;
|
||||
let end = textRange.endOffset;
|
||||
links.forEach((link) => {
|
||||
start = Math.min(start, link.startIndex);
|
||||
end = Math.max(end, link.endIndex + 1);
|
||||
});
|
||||
return {
|
||||
startOffset: start,
|
||||
endOffset: end,
|
||||
collapsed: start === end,
|
||||
label: BuildTextUtils.transform.getPlainText(body.dataStream.slice(start, end)),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private get _editPopup() {
|
||||
const popup: ICanvasPopup = {
|
||||
componentKey: CellLinkEdit.componentKey,
|
||||
direction: 'vertical',
|
||||
onClickOutside: () => {
|
||||
this.endEditing();
|
||||
},
|
||||
onContextMenu: () => {
|
||||
this.endEditing();
|
||||
},
|
||||
hiddenType: 'hide',
|
||||
};
|
||||
return popup;
|
||||
}
|
||||
|
||||
startAddEditing(link: IHyperLinkEditing) {
|
||||
const { unitId, subUnitId, type } = link;
|
||||
if (type === HyperLinkEditSourceType.EDITING) {
|
||||
const range = this._getEditingRange();
|
||||
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._textSelectionManagerService.replaceDocRanges([{ ...range }], { unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY });
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
if (!currentRender) {
|
||||
return;
|
||||
}
|
||||
const rects = calcDocRangePositions(range, currentRender);
|
||||
if (!rects?.length) {
|
||||
return;
|
||||
}
|
||||
this._currentEditingPopup = this._sheetCanvasPopManagerService.attachPopupToAbsolutePosition(
|
||||
rects.pop()!,
|
||||
this._editPopup,
|
||||
unitId,
|
||||
subUnitId
|
||||
);
|
||||
|
||||
this._currentEditing$.next({
|
||||
...link,
|
||||
label: range?.label ?? '',
|
||||
});
|
||||
} else {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
const cell = worksheet?.getCellRaw(link.row, link.col);
|
||||
const label = cell?.p ? BuildTextUtils.transform.getPlainText(cell.p.body?.dataStream ?? '') : (cell?.v ?? '').toString();
|
||||
this._currentEditingPopup = this._sheetCanvasPopManagerService.attachPopupToCell(
|
||||
link.row,
|
||||
link.col,
|
||||
this._editPopup,
|
||||
unitId,
|
||||
subUnitId
|
||||
);
|
||||
this._currentEditing$.next({
|
||||
...link,
|
||||
label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startEditing(link: Required<IHyperLinkEditing>) {
|
||||
this._currentEditingPopup?.dispose();
|
||||
this.hideCurrentPopup(undefined, true);
|
||||
|
||||
const { unitId, subUnitId } = link;
|
||||
let customRange;
|
||||
let label;
|
||||
if (link.type === HyperLinkEditSourceType.EDITING) {
|
||||
const customRangeInfo = getEditingCustomRangePosition(this._injector, link.unitId, link.subUnitId, link.row, link.col, link.customRangeId);
|
||||
if (!customRangeInfo || !customRangeInfo.rects?.length) {
|
||||
return;
|
||||
}
|
||||
customRange = customRangeInfo.customRange;
|
||||
label = customRangeInfo.label;
|
||||
this._textSelectionManagerService.replaceDocRanges([
|
||||
{
|
||||
startOffset: customRange.startIndex,
|
||||
endOffset: customRange.endIndex + 1,
|
||||
},
|
||||
]);
|
||||
this._currentEditingPopup = this._sheetCanvasPopManagerService.attachPopupToAbsolutePosition(
|
||||
customRangeInfo.rects.pop()!,
|
||||
this._editPopup,
|
||||
unitId,
|
||||
subUnitId
|
||||
);
|
||||
} else {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
const cell = worksheet?.getCellRaw(link.row, link.col);
|
||||
const style = workbook?.getStyles().getStyleByCell(cell);
|
||||
const tr = style?.tr;
|
||||
|
||||
const customRangeInfo = getCustomRangePosition(this._injector, link.unitId, link.subUnitId, link.row, link.col, link.customRangeId);
|
||||
if (!customRangeInfo || !customRangeInfo.rects?.length) {
|
||||
return;
|
||||
}
|
||||
customRange = customRangeInfo.customRange;
|
||||
label = customRangeInfo.label;
|
||||
if (tr) {
|
||||
this._currentEditingPopup = this._sheetCanvasPopManagerService.attachPopupToCell(
|
||||
link.row,
|
||||
link.col,
|
||||
this._editPopup,
|
||||
unitId,
|
||||
subUnitId
|
||||
);
|
||||
} else {
|
||||
this._currentEditingPopup = this._sheetCanvasPopManagerService.attachPopupByPosition(
|
||||
customRangeInfo.rects.pop()!,
|
||||
this._editPopup,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
row: link.row,
|
||||
col: link.col,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const editing = {
|
||||
...link,
|
||||
customRange,
|
||||
label,
|
||||
};
|
||||
this._currentEditing$.next(editing);
|
||||
}
|
||||
|
||||
endEditing(type?: HyperLinkEditSourceType) {
|
||||
if (this.getIsKeepVisible()) {
|
||||
return;
|
||||
}
|
||||
const current = this._currentEditing$.getValue();
|
||||
if (current && (!type || type === current.type)) {
|
||||
this._currentEditingPopup?.dispose();
|
||||
this._currentEditing$.next(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
packages/sheets-hyper-link-ui/src/services/resolver.service.ts
Normal file
223
packages/sheets-hyper-link-ui/src/services/resolver.service.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { ISetSelectionsOperationParams } from '@univerjs/sheets';
|
||||
import type { ISheetHyperLinkInfo, ISheetUrlParams } from '@univerjs/sheets-hyper-link';
|
||||
import type { IUniverSheetsHyperLinkUIConfig } from '../config/config';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
Inject,
|
||||
isSafeUrl,
|
||||
isValidRange,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
RANGE_TYPE,
|
||||
Rectangle,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { deserializeRangeWithSheet, IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import { SetSelectionsOperation, SetWorksheetActiveOperation } from '@univerjs/sheets';
|
||||
import { ERROR_RANGE, SheetHyperLinkType } from '@univerjs/sheets-hyper-link';
|
||||
import { ScrollToRangeOperation } from '@univerjs/sheets-ui';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
|
||||
function getContainRange(range: IRange, worksheet: Worksheet) {
|
||||
const mergedCells = worksheet.getMergeData();
|
||||
const maxCol = worksheet.getMaxColumns() - 1;
|
||||
const maxRow = worksheet.getMaxRows() - 1;
|
||||
if (maxCol < range.endColumn) {
|
||||
range.endColumn = maxCol;
|
||||
}
|
||||
|
||||
if (maxRow < range.endRow) {
|
||||
range.endRow = maxRow;
|
||||
}
|
||||
|
||||
if (range.rangeType === RANGE_TYPE.COLUMN || RANGE_TYPE.ROW) {
|
||||
return range;
|
||||
}
|
||||
|
||||
const relativeCells: IRange[] = [];
|
||||
mergedCells.forEach((cell) => {
|
||||
if (Rectangle.intersects(range, cell)) {
|
||||
relativeCells.push(cell);
|
||||
}
|
||||
});
|
||||
|
||||
return Rectangle.realUnion(range, ...relativeCells);
|
||||
}
|
||||
|
||||
export class SheetsHyperLinkResolverService {
|
||||
constructor(
|
||||
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private _commandService: ICommandService,
|
||||
@IDefinedNamesService private _definedNamesService: IDefinedNamesService,
|
||||
@IMessageService private _messageService: IMessageService,
|
||||
@Inject(LocaleService) private _localeService: LocaleService,
|
||||
@IConfigService private _configService: IConfigService
|
||||
) { }
|
||||
|
||||
navigate(info: ISheetHyperLinkInfo): void {
|
||||
switch (info.type) {
|
||||
case SheetHyperLinkType.URL:
|
||||
this.navigateToOtherWebsite(info.url);
|
||||
break;
|
||||
default:
|
||||
this._navigateToUniver(info.searchObj!);
|
||||
}
|
||||
}
|
||||
|
||||
private _navigateToUniver(params: ISheetUrlParams) {
|
||||
// NOTE: should we always use current unit and active worksheet?
|
||||
|
||||
const { gid, range, rangeid } = params;
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
if (rangeid) {
|
||||
const item = this._definedNamesService.getValueById(unitId, rangeid);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { formulaOrRefString } = item;
|
||||
const worksheet = this._definedNamesService.getWorksheetByRef(unitId, formulaOrRefString);
|
||||
|
||||
if (!worksheet) {
|
||||
this._messageService.show({
|
||||
content: this._localeService.t<LocaleKey>('sheets-hyper-link-ui.message.refError'),
|
||||
type: MessageType.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isHidden = worksheet.isSheetHidden();
|
||||
|
||||
// The worksheet may be hidden
|
||||
if (isHidden) {
|
||||
this._messageService.show({
|
||||
content: this._localeService.t<LocaleKey>('sheets-hyper-link-ui.message.hiddenSheet'),
|
||||
type: MessageType.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.navigateToDefineName(unitId, rangeid);
|
||||
}
|
||||
|
||||
if (!gid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (range) {
|
||||
const rangeInfo = deserializeRangeWithSheet(range);
|
||||
if (isValidRange(rangeInfo.range) && range !== ERROR_RANGE) {
|
||||
this.navigateToRange(unitId, gid, rangeInfo.range);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.navigateToSheetById(unitId, gid);
|
||||
}
|
||||
|
||||
async navigateToRange(unitId: string, subUnitId: string, range: IRange, forceTop?: boolean) {
|
||||
const worksheet = await this.navigateToSheetById(unitId, subUnitId);
|
||||
if (worksheet) {
|
||||
const realRange = getContainRange(range, worksheet);
|
||||
await this._commandService.executeCommand(
|
||||
SetSelectionsOperation.id,
|
||||
{
|
||||
unitId,
|
||||
subUnitId,
|
||||
|
||||
selections: [{
|
||||
range: realRange,
|
||||
primary: null,
|
||||
}],
|
||||
} as ISetSelectionsOperationParams
|
||||
);
|
||||
await this._commandService.executeCommand(ScrollToRangeOperation.id, {
|
||||
range: realRange,
|
||||
forceTop,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async navigateToSheetById(unitId: string, subUnitId: string) {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return false;
|
||||
}
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (worksheet.getSheetId() === subUnitId) {
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
const targetSheet = workbook.getSheetBySheetId(subUnitId);
|
||||
|
||||
if (!targetSheet) {
|
||||
this._messageService.show({
|
||||
content: this._localeService.t<LocaleKey>('sheets-hyper-link-ui.message.noSheet'),
|
||||
type: MessageType.Error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (workbook.getHiddenWorksheets().indexOf(subUnitId) > -1) {
|
||||
this._messageService.show({
|
||||
content: this._localeService.t<LocaleKey>('sheets-hyper-link-ui.message.hiddenSheet'),
|
||||
type: MessageType.Error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await this._commandService.executeCommand(SetWorksheetActiveOperation.id, { unitId, subUnitId })) {
|
||||
return targetSheet;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async navigateToDefineName(unitId: string, rangeId: string) {
|
||||
this._definedNamesService.focusRange(unitId, rangeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async navigateToOtherWebsite(url: string) {
|
||||
if (!isSafeUrl(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this._configService.getConfig<IUniverSheetsHyperLinkUIConfig>(SHEETS_HYPER_LINK_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
if (config?.urlHandler?.navigateToOtherWebsite) {
|
||||
return config.urlHandler.navigateToOtherWebsite(url);
|
||||
}
|
||||
|
||||
window.open(url, '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
@@ -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 { ISheetHyperLink } from '@univerjs/sheets-hyper-link';
|
||||
import type * as React from 'react';
|
||||
import { Disposable } from '@univerjs/core';
|
||||
import { SheetHyperLinkType } from '@univerjs/sheets-hyper-link';
|
||||
|
||||
export interface ICustomHyperLinkFormProps {
|
||||
linkId: string;
|
||||
payload: string;
|
||||
display: string;
|
||||
getIsDisplaySyncedWithPayload: () => boolean;
|
||||
showError: boolean;
|
||||
setDisplaySyncedWithPayload: (synced: boolean) => void;
|
||||
setDisplay: (display: string) => void;
|
||||
setPayload: (payload: string) => void;
|
||||
}
|
||||
|
||||
export interface ICustomHyperLinkView {
|
||||
type: string;
|
||||
option: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
Form: React.FC<ICustomHyperLinkFormProps>;
|
||||
convert: (link: ISheetHyperLink) => { display: string; payload: string; type: string };
|
||||
match: (link: ISheetHyperLink) => boolean;
|
||||
}
|
||||
|
||||
export class SheetsHyperLinkSidePanelService extends Disposable {
|
||||
private _customHyperLinks = new Map<string, ICustomHyperLinkView>();
|
||||
|
||||
isBuiltInLinkType(type: SheetHyperLinkType | string) {
|
||||
return type !== SheetHyperLinkType.URL;
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return Array.from(this._customHyperLinks.values()).map(({ option }) => option);
|
||||
}
|
||||
|
||||
findCustomHyperLink(link: ISheetHyperLink) {
|
||||
const customLink = Array.from(this._customHyperLinks.values()).find((item) => item.match(link));
|
||||
|
||||
return customLink;
|
||||
}
|
||||
|
||||
registerCustomHyperLink(customHyperLink: ICustomHyperLinkView) {
|
||||
this._customHyperLinks.set(customHyperLink.type, customHyperLink);
|
||||
}
|
||||
|
||||
getCustomHyperLink(type: string) {
|
||||
return this._customHyperLinks.get(type);
|
||||
}
|
||||
|
||||
removeCustomHyperLink(type: string) {
|
||||
const { _customHyperLinks } = this;
|
||||
_customHyperLinks.delete(type);
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this._customHyperLinks.clear();
|
||||
}
|
||||
}
|
||||
17
packages/sheets-hyper-link-ui/src/types/const.ts
Normal file
17
packages/sheets-hyper-link-ui/src/types/const.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 SHEET_HYPER_LINK_UI_PLUGIN = 'SHEET_HYPER_LINK_UI_PLUGIN';
|
||||
20
packages/sheets-hyper-link-ui/src/types/enums/edit-source.ts
Normal file
20
packages/sheets-hyper-link-ui/src/types/enums/edit-source.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export enum HyperLinkEditSourceType {
|
||||
EDITING = 'editing',
|
||||
VIEWING = 'viewing',
|
||||
}
|
||||
17
packages/sheets-hyper-link-ui/src/types/enums/index.ts
Normal file
17
packages/sheets-hyper-link-ui/src/types/enums/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 { HyperLinkEditSourceType } from './edit-source';
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { DataValidationType, IUniverInstanceService } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SheetDataValidationModel } from '@univerjs/sheets-data-validation';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DisableLinkType, getShouldDisableCellLink, getShouldDisableCurrentCellLink, shouldDisableAddLink } from '../index';
|
||||
|
||||
function createAccessor(map: Array<[unknown, unknown]>) {
|
||||
const tokenMap = new Map(map);
|
||||
return {
|
||||
get: (token: unknown) => tokenMap.get(token),
|
||||
has: (token: unknown) => tokenMap.has(token),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('hyper-link utils', () => {
|
||||
it('should disable cell links for formulas, custom blocks and data validation rules', () => {
|
||||
const worksheet = {
|
||||
getCell: () => ({ f: '=SUM(A1:A2)' }),
|
||||
getUnitId: () => 'unit-1',
|
||||
getSheetId: () => 'sheet-1',
|
||||
};
|
||||
|
||||
expect(getShouldDisableCellLink(createAccessor([]), worksheet as any, 0, 0)).toBe(DisableLinkType.DISABLED_BY_CELL);
|
||||
|
||||
const worksheetWithBlocks = {
|
||||
...worksheet,
|
||||
getCell: () => ({ p: { body: { customBlocks: [{}] } } }),
|
||||
};
|
||||
expect(getShouldDisableCellLink(createAccessor([]), worksheetWithBlocks as any, 0, 0)).toBe(DisableLinkType.DISABLED_BY_CELL);
|
||||
|
||||
const accessor = createAccessor([
|
||||
[SheetDataValidationModel, { getRuleByLocation: () => ({ type: DataValidationType.CHECKBOX }) }],
|
||||
]);
|
||||
const plainWorksheet = {
|
||||
...worksheet,
|
||||
getCell: () => ({}),
|
||||
};
|
||||
|
||||
expect(getShouldDisableCellLink(accessor, plainWorksheet as any, 1, 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow editing-only links for drawing cells and inspect the current selection', () => {
|
||||
const worksheet = {
|
||||
getCell: () => ({ p: { drawingsOrder: ['drawing-1'] } }),
|
||||
getUnitId: () => 'unit-1',
|
||||
getSheetId: () => 'sheet-1',
|
||||
};
|
||||
|
||||
expect(getShouldDisableCellLink(createAccessor([]), worksheet as any, 0, 0)).toBe(DisableLinkType.ALLOW_ON_EDITING);
|
||||
|
||||
const currentAccessor = createAccessor([
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: () => ({ getActiveSheet: () => worksheet }) }],
|
||||
[SheetsSelectionsService, { getCurrentSelections: () => [{ range: { startRow: 0, startColumn: 0 } }] }],
|
||||
]);
|
||||
|
||||
expect(getShouldDisableCurrentCellLink(currentAccessor)).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable add-link when editor selection is missing and allow it with valid rich text context', () => {
|
||||
const noSelectionAccessor = createAccessor([
|
||||
[DocSelectionManagerService, { getTextRanges: () => [] }],
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: () => null }],
|
||||
]);
|
||||
expect(shouldDisableAddLink(noSelectionAccessor)).toBe(true);
|
||||
|
||||
const validAccessor = createAccessor([
|
||||
[DocSelectionManagerService, { getTextRanges: () => [{ collapsed: false, segmentId: 'body' }] }],
|
||||
[IUniverInstanceService, {
|
||||
getCurrentUnitOfType: () => ({
|
||||
getSelfOrHeaderFooterModel: () => ({ getBody: () => ({ dataStream: 'hello\r\n' }) }),
|
||||
}),
|
||||
}],
|
||||
]);
|
||||
|
||||
expect(shouldDisableAddLink(validAccessor)).toBe(false);
|
||||
});
|
||||
});
|
||||
91
packages/sheets-hyper-link-ui/src/utils/index.ts
Normal file
91
packages/sheets-hyper-link-ui/src/utils/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, IAccessor, Workbook, Worksheet } from '@univerjs/core';
|
||||
import { DataValidationType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SheetDataValidationModel } from '@univerjs/sheets-data-validation';
|
||||
|
||||
export enum DisableLinkType {
|
||||
ALLOWED = 0,
|
||||
DISABLED_BY_CELL = 1,
|
||||
ALLOW_ON_EDITING = 2,
|
||||
}
|
||||
const disables = new Set<string>([
|
||||
DataValidationType.CHECKBOX,
|
||||
DataValidationType.LIST,
|
||||
DataValidationType.LIST_MULTIPLE,
|
||||
]);
|
||||
|
||||
export const getShouldDisableCellLink = (accessor: IAccessor, worksheet: Worksheet, row: number, col: number) => {
|
||||
const cell = worksheet.getCell(row, col);
|
||||
if (cell?.f || cell?.si) {
|
||||
return DisableLinkType.DISABLED_BY_CELL;
|
||||
}
|
||||
|
||||
if (cell?.p?.body?.customBlocks?.length) {
|
||||
return DisableLinkType.DISABLED_BY_CELL;
|
||||
}
|
||||
|
||||
const dataValidationModel = accessor.has(SheetDataValidationModel) ? accessor.get(SheetDataValidationModel) : null;
|
||||
const rule = dataValidationModel?.getRuleByLocation(worksheet.getUnitId(), worksheet.getSheetId(), row, col);
|
||||
if (rule && disables.has(rule.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cell?.p?.drawingsOrder?.length) {
|
||||
return DisableLinkType.ALLOW_ON_EDITING;
|
||||
}
|
||||
|
||||
return DisableLinkType.ALLOWED;
|
||||
};
|
||||
|
||||
export const getShouldDisableCurrentCellLink = (accessor: IAccessor) => {
|
||||
const unit = accessor.get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!unit) {
|
||||
return true;
|
||||
}
|
||||
const worksheet = unit.getActiveSheet();
|
||||
const selections = accessor.get(SheetsSelectionsService).getCurrentSelections();
|
||||
if (!selections.length) {
|
||||
return true;
|
||||
}
|
||||
const row = selections[0].range.startRow;
|
||||
const col = selections[0].range.startColumn;
|
||||
return getShouldDisableCellLink(accessor, worksheet, row, col) === DisableLinkType.DISABLED_BY_CELL;
|
||||
};
|
||||
|
||||
export const shouldDisableAddLink = (accessor: IAccessor) => {
|
||||
const textSelectionService = accessor.get(DocSelectionManagerService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const textRanges = textSelectionService.getTextRanges();
|
||||
if (!textRanges?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const doc = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!doc || textRanges.every((range) => range.collapsed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const body = doc.getSelfOrHeaderFooterModel(textRanges[0].segmentId)?.getBody();
|
||||
if (!body) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
592
packages/sheets-hyper-link-ui/src/views/CellLinkEdit.tsx
Normal file
592
packages/sheets-hyper-link-ui/src/views/CellLinkEdit.tsx
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
BuildTextUtils,
|
||||
ColorKit,
|
||||
CustomRangeType,
|
||||
DataStreamTreeTokenType,
|
||||
DisposableCollection,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
ThemeService,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { ActionRow, borderClassName, Button, clsx, FormLayout, Input, Select } from '@univerjs/design';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import {
|
||||
deserializeRangeWithSheet,
|
||||
IDefinedNamesService,
|
||||
serializeRange,
|
||||
serializeRangeWithSheet,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { RangeSelector } from '@univerjs/sheets-formula-ui';
|
||||
import {
|
||||
AddHyperLinkCommand,
|
||||
AddRichHyperLinkCommand,
|
||||
SheetHyperLinkType,
|
||||
SheetsHyperLinkParserService,
|
||||
UpdateHyperLinkCommand,
|
||||
UpdateRichHyperLinkCommand,
|
||||
} from '@univerjs/sheets-hyper-link';
|
||||
import { IEditorBridgeService, IMarkSelectionService, ScrollToRangeOperation } from '@univerjs/sheets-ui';
|
||||
import { KeyCode, useDependency, useEvent, useObservable } from '@univerjs/ui';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CloseHyperLinkPopupOperation } from '../commands/operations/popup.operations';
|
||||
import { isLegalLink, serializeUrl } from '../common/util';
|
||||
import { ISheetsHyperLinkPopupService } from '../services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from '../services/resolver.service';
|
||||
import { SheetsHyperLinkSidePanelService } from '../services/side-panel.service';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
import { isBlankInput, resolveRangePayload } from './CellLinkEdit/utils';
|
||||
|
||||
export const CellLinkEdit = () => {
|
||||
const [id, setId] = useState('');
|
||||
const [display, setDisplay] = useState('');
|
||||
const [showLabel, setShowLabel] = useState(true);
|
||||
const [type, setType] = useState<SheetHyperLinkType | string>(SheetHyperLinkType.URL);
|
||||
const [payload, setPayload] = useState('');
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const definedNameService = useDependency(IDefinedNamesService);
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const popupService = useDependency(ISheetsHyperLinkPopupService);
|
||||
const editing = useObservable(popupService.currentEditing$);
|
||||
const parserService = useDependency(SheetsHyperLinkParserService);
|
||||
const resolverService = useDependency(SheetsHyperLinkResolverService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const sidePanelService = useDependency(SheetsHyperLinkSidePanelService);
|
||||
const sidePanelOptions = useMemo(() => sidePanelService.getOptions(), [sidePanelService]);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const markSelectionService = useDependency(IMarkSelectionService);
|
||||
const textSelectionService = useDependency(DocSelectionManagerService);
|
||||
const themeService = useDependency(ThemeService);
|
||||
const [selectorDialogVisible, setSelectorDialogVisible] = useState(false);
|
||||
const sheetsSelectionService = useDependency(SheetsSelectionsService);
|
||||
const selections = useMemo(() => sheetsSelectionService.getCurrentSelections(), []);
|
||||
|
||||
const customHyperLinkSidePanel = useMemo(() => {
|
||||
if (sidePanelService.isBuiltInLinkType(type)) {
|
||||
return;
|
||||
}
|
||||
return sidePanelService.getCustomHyperLink(type);
|
||||
}, [sidePanelService, type]);
|
||||
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
const [isFocusRangeSelector, setIsFocusRangeSelector] = useState(false);
|
||||
|
||||
const isDisplaySyncedWithPayloadRef = useRef(false);
|
||||
const getIsDisplaySyncedWithPayload = useEvent(() => isDisplaySyncedWithPayloadRef.current);
|
||||
const setDisplaySyncedWithPayload = useEvent((synced: boolean) => {
|
||||
isDisplaySyncedWithPayloadRef.current = synced;
|
||||
});
|
||||
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
|
||||
const subUnitId = workbook?.getActiveSheet().getSheetId() || '';
|
||||
// to polyfill the display value on old version data
|
||||
// case split tag is no longer needed
|
||||
const setNormalizedDisplay = useCallback((value: string) => {
|
||||
setDisplay(value.replaceAll(DataStreamTreeTokenType.CUSTOM_RANGE_START, '').replaceAll(DataStreamTreeTokenType.CUSTOM_RANGE_END, ''));
|
||||
}, [setDisplay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing?.row !== undefined && editing.col !== undefined) {
|
||||
const { customRange, row, col } = editing;
|
||||
let { label } = editing;
|
||||
if (typeof label === 'number') {
|
||||
label = `${label}`;
|
||||
}
|
||||
|
||||
let link;
|
||||
if (customRange) {
|
||||
link = {
|
||||
id: customRange?.rangeId ?? '',
|
||||
display: label ?? '',
|
||||
payload: customRange?.properties?.url ?? '',
|
||||
row,
|
||||
column: col,
|
||||
};
|
||||
} else {
|
||||
if (editing.type === HyperLinkEditSourceType.VIEWING) {
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(editing.unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(editing.subUnitId);
|
||||
const cell = worksheet?.getCellRaw(editing.row, editing.col);
|
||||
const range = cell?.p?.body?.customRanges?.find((range) => range.rangeType === CustomRangeType.HYPERLINK && range.properties?.url);
|
||||
const cellValue = cell?.v;
|
||||
if (cell && (!BuildTextUtils.transform.isEmptyDocument(cell.p?.body?.dataStream) || Tools.isDefine(cellValue))) {
|
||||
setShowLabel(false);
|
||||
}
|
||||
link = {
|
||||
id: '',
|
||||
display: '',
|
||||
payload: range?.properties?.url ?? '',
|
||||
row,
|
||||
column: col,
|
||||
};
|
||||
} else {
|
||||
const doc = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const currentSelection = textSelectionService.getActiveTextRange();
|
||||
const body = doc?.getBody();
|
||||
const selection = currentSelection && body ? currentSelection : null;
|
||||
const customRange = selection && BuildTextUtils.customRange.getCustomRangesInterestsWithSelection(selection, body?.customRanges ?? [])?.[0];
|
||||
|
||||
setShowLabel(false);
|
||||
link = {
|
||||
id: '',
|
||||
display: label ?? '',
|
||||
payload: customRange?.properties?.url ?? '',
|
||||
row,
|
||||
column: col,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setId(link.id);
|
||||
const customLink = sidePanelService.findCustomHyperLink(link);
|
||||
if (customLink) {
|
||||
const customLinkInfo = customLink.convert(link);
|
||||
setType(customLinkInfo.type);
|
||||
setPayload(customLinkInfo.payload);
|
||||
setNormalizedDisplay(customLinkInfo.display);
|
||||
return;
|
||||
}
|
||||
setNormalizedDisplay(link.display);
|
||||
const linkInfo = parserService.parseHyperLink(link.payload);
|
||||
setType(linkInfo.type === SheetHyperLinkType.INVALID ? SheetHyperLinkType.RANGE : linkInfo.type);
|
||||
switch (linkInfo.type) {
|
||||
case SheetHyperLinkType.URL: {
|
||||
setPayload(linkInfo.url);
|
||||
if (linkInfo.url === link.display) {
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SheetHyperLinkType.RANGE: {
|
||||
const params = linkInfo.searchObj!;
|
||||
const sheetName = params.gid ?
|
||||
univerInstanceService
|
||||
.getUnit<Workbook>(editing.unitId)
|
||||
?.getSheetBySheetId(params.gid)
|
||||
?.getName()
|
||||
?? ''
|
||||
: '';
|
||||
const payload = (serializeRangeWithSheet(sheetName, deserializeRangeWithSheet(params.range!).range));
|
||||
setPayload(payload);
|
||||
if (payload === link.display) {
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SheetHyperLinkType.SHEET: {
|
||||
const params = linkInfo.searchObj!;
|
||||
setPayload(params.gid!);
|
||||
break;
|
||||
}
|
||||
case SheetHyperLinkType.DEFINE_NAME: {
|
||||
const params = linkInfo.searchObj!;
|
||||
setPayload(params.rangeid!);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
setPayload('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [editing, resolverService, sidePanelService, textSelectionService, univerInstanceService]);
|
||||
|
||||
useEffect(() => {
|
||||
let id: Nullable<string> = null;
|
||||
if (editing && !editing.customRangeId && editing.type === HyperLinkEditSourceType.VIEWING && Tools.isDefine(editing.row) && Tools.isDefine(editing.col)) {
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(editing.unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(editing.subUnitId);
|
||||
const mergeInfo = worksheet?.getMergedCell(editing.row, editing.col);
|
||||
const color = new ColorKit(themeService.getColorFromTheme('primary.600')).toRgb();
|
||||
id = markSelectionService.addShape(
|
||||
{
|
||||
range: mergeInfo ?? {
|
||||
startColumn: editing.col,
|
||||
endColumn: editing.col,
|
||||
startRow: editing.row,
|
||||
endRow: editing.row,
|
||||
},
|
||||
style: {
|
||||
// hasAutoFill: false,
|
||||
fill: `rgb(${color.r}, ${color.g}, ${color.b}, 0.12)`,
|
||||
strokeWidth: 1,
|
||||
stroke: '#FFBD37',
|
||||
widgets: {},
|
||||
},
|
||||
primary: null,
|
||||
},
|
||||
[],
|
||||
-1
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
if (id) {
|
||||
markSelectionService.removeShape(id);
|
||||
}
|
||||
};
|
||||
}, [editing, markSelectionService, themeService, univerInstanceService]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsFocusRangeSelector(type === SheetHyperLinkType.RANGE);
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
const render = renderManagerService.getRenderUnitById(editorBridgeService.getCurrentEditorId());
|
||||
const disposeCollection = new DisposableCollection();
|
||||
|
||||
if (render) {
|
||||
const selectionRenderService = render.with(DocSelectionRenderService);
|
||||
selectionRenderService.setReserveRangesStatus(true);
|
||||
disposeCollection.add(() => {
|
||||
selectionRenderService.setReserveRangesStatus(false);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
editorBridgeService.disableForceKeepVisible();
|
||||
disposeCollection.dispose();
|
||||
};
|
||||
}, [editorBridgeService, renderManagerService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusRangeSelector) {
|
||||
popupService.setIsKeepVisible(isFocusRangeSelector);
|
||||
}
|
||||
popupService.setIsKeepVisible(selectorDialogVisible);
|
||||
|
||||
return () => {
|
||||
popupService.setIsKeepVisible(false);
|
||||
};
|
||||
}, [isFocusRangeSelector, selectorDialogVisible, popupService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusRangeSelector) {
|
||||
editorBridgeService.enableForceKeepVisible();
|
||||
|
||||
return () => {
|
||||
editorBridgeService.disableForceKeepVisible();
|
||||
};
|
||||
}
|
||||
}, [isFocusRangeSelector, editorBridgeService]);
|
||||
|
||||
const linkTypeOptions: Array<{
|
||||
label: string;
|
||||
value: SheetHyperLinkType | string;
|
||||
}> = [
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.link'),
|
||||
value: SheetHyperLinkType.URL,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.range'),
|
||||
value: SheetHyperLinkType.RANGE,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.worksheet'),
|
||||
value: SheetHyperLinkType.SHEET,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.definedName'),
|
||||
value: SheetHyperLinkType.DEFINE_NAME,
|
||||
},
|
||||
...sidePanelOptions,
|
||||
];
|
||||
|
||||
const handleRangeChange = useEvent((rangeText: string) => {
|
||||
const newPayload = resolveRangePayload(rangeText, workbook?.getActiveSheet()?.getName() || '');
|
||||
if (!newPayload) {
|
||||
setPayload('');
|
||||
return;
|
||||
}
|
||||
|
||||
setPayload(newPayload);
|
||||
|
||||
if (getIsDisplaySyncedWithPayload() || !display) {
|
||||
setNormalizedDisplay(newPayload);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hiddens = workbook.getHiddenWorksheets();
|
||||
const sheetsOption = workbook.getSheets().map((sheet) => ({ label: sheet.getName(), value: sheet.getSheetId() })).filter((opt) => hiddens.indexOf(opt.value) === -1);
|
||||
const definedNames = Object.values(definedNameService.getDefinedNameMap(workbook.getUnitId()) ?? {}).map((value) => ({
|
||||
label: value.name,
|
||||
value: value.id,
|
||||
}));
|
||||
|
||||
const formatUrl = (type: SheetHyperLinkType | string, payload: string) => {
|
||||
if (type === SheetHyperLinkType.URL) {
|
||||
return serializeUrl(payload);
|
||||
}
|
||||
|
||||
if (type === SheetHyperLinkType.RANGE) {
|
||||
const info = deserializeRangeWithSheet(payload);
|
||||
const worksheet = workbook.getSheetBySheetName(info.sheetName);
|
||||
if (worksheet) {
|
||||
return `#gid=${worksheet.getSheetId()}&range=${serializeRange(info.range)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `#${type}=${payload}`;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if ((showLabel && isBlankInput(display)) || !payload || (type === SheetHyperLinkType.URL && !isLegalLink(payload))) {
|
||||
setShowError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
if (id) {
|
||||
const commandId = editing.type === HyperLinkEditSourceType.EDITING ? UpdateRichHyperLinkCommand.id : UpdateHyperLinkCommand.id;
|
||||
await commandService.executeCommand(commandId, {
|
||||
id,
|
||||
unitId: editing.unitId,
|
||||
subUnitId: editing.subUnitId,
|
||||
payload: {
|
||||
display: showLabel ? display : '',
|
||||
payload: formatUrl(type, payload),
|
||||
},
|
||||
row: editing.row,
|
||||
column: editing.col,
|
||||
documentId: editorBridgeService.getCurrentEditorId(),
|
||||
});
|
||||
} else {
|
||||
const commandId = editing.type === HyperLinkEditSourceType.EDITING ? AddRichHyperLinkCommand.id : AddHyperLinkCommand.id;
|
||||
await commandService.executeCommand(commandId, {
|
||||
unitId: editing.unitId,
|
||||
subUnitId: editing.subUnitId,
|
||||
link: {
|
||||
id: generateRandomId(),
|
||||
row: editing.row,
|
||||
column: editing.col,
|
||||
payload: formatUrl(type, payload),
|
||||
display: showLabel ? display : '',
|
||||
},
|
||||
documentId: editorBridgeService.getCurrentEditorId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (editing?.type === HyperLinkEditSourceType.VIEWING) {
|
||||
await commandService.executeCommand(SetWorksheetActiveOperation.id, {
|
||||
unitId: editing.unitId,
|
||||
subUnitId: editing.subUnitId,
|
||||
});
|
||||
|
||||
const GAP = 1;
|
||||
await commandService.executeCommand(ScrollToRangeOperation.id, {
|
||||
range: {
|
||||
startRow: Math.max(editing.row - GAP, 0),
|
||||
endRow: editing.row + GAP,
|
||||
startColumn: Math.max(editing.col - GAP, 0),
|
||||
endColumn: editing.col + GAP,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await commandService.executeCommand(CloseHyperLinkPopupOperation.id);
|
||||
};
|
||||
|
||||
if (!editing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`
|
||||
univer-box-border univer-w-[296px] univer-rounded-xl univer-bg-gray-0 univer-p-4 univer-shadow-md
|
||||
dark:!univer-bg-gray-900
|
||||
`,
|
||||
borderClassName
|
||||
)}
|
||||
>
|
||||
{showLabel
|
||||
? (
|
||||
<FormLayout
|
||||
label={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.label')}
|
||||
error={showError && isBlankInput(display) ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.inputError') : ''}
|
||||
>
|
||||
<Input
|
||||
value={display}
|
||||
onChange={(v) => {
|
||||
setNormalizedDisplay(v);
|
||||
setDisplaySyncedWithPayload(false);
|
||||
}}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.labelPlaceholder')}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)
|
||||
: null}
|
||||
<FormLayout label={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.type')}>
|
||||
<Select
|
||||
className="univer-w-full"
|
||||
options={linkTypeOptions}
|
||||
value={type}
|
||||
onChange={(newType) => {
|
||||
setType(newType as SheetHyperLinkType);
|
||||
setPayload('');
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
{type === SheetHyperLinkType.URL && (
|
||||
<FormLayout
|
||||
error={showError ? !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.inputError') : !isLegalLink(payload) ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.linkError') : '' : ''}
|
||||
>
|
||||
<Input
|
||||
dir="ltr"
|
||||
value={payload}
|
||||
onChange={(newLink) => {
|
||||
setPayload(newLink);
|
||||
if (newLink && (getIsDisplaySyncedWithPayload() || !display || display === newLink)) {
|
||||
setNormalizedDisplay(newLink);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
}}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.linkPlaceholder')}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{type === SheetHyperLinkType.RANGE && (
|
||||
<FormLayout error={showError && !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.inputError') : ''}>
|
||||
<RangeSelector
|
||||
unitId={workbook.getUnitId()}
|
||||
subUnitId={subUnitId}
|
||||
maxRangeCount={1}
|
||||
supportAcrossSheet
|
||||
initialValue={payload}
|
||||
resetRange={selections as ISelectionWithStyle[]}
|
||||
onChange={(_, text) => handleRangeChange(text)}
|
||||
onRangeSelectorDialogVisibleChange={async (visible) => {
|
||||
setSelectorDialogVisible(visible);
|
||||
if (visible) {
|
||||
if (editing.type !== HyperLinkEditSourceType.VIEWING) {
|
||||
editorBridgeService.enableForceKeepVisible();
|
||||
}
|
||||
} else {
|
||||
await resolverService.navigateToRange(editing.unitId, editing.subUnitId, { startRow: editing.row, endRow: editing.row, startColumn: editing.col, endColumn: editing.col }, true);
|
||||
editorBridgeService.disableForceKeepVisible();
|
||||
}
|
||||
}}
|
||||
onFocusChange={(focus) => setIsFocusRangeSelector(focus)}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{type === SheetHyperLinkType.SHEET && (
|
||||
<FormLayout error={showError && !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.selectError') : ''}>
|
||||
<Select
|
||||
className="univer-w-full"
|
||||
options={sheetsOption}
|
||||
value={payload}
|
||||
onChange={(newPayload) => {
|
||||
setPayload(newPayload);
|
||||
const label = sheetsOption.find((i) => i.value === newPayload)?.label;
|
||||
const oldLabel = sheetsOption.find((i) => i.value === payload)?.label;
|
||||
if (label && (getIsDisplaySyncedWithPayload() || !display || display === oldLabel)) {
|
||||
setNormalizedDisplay(label);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{type === SheetHyperLinkType.DEFINE_NAME && (
|
||||
<FormLayout error={showError && !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.selectError') : ''}>
|
||||
<Select
|
||||
className="univer-w-full"
|
||||
options={definedNames}
|
||||
value={payload}
|
||||
onChange={(newValue) => {
|
||||
setPayload(newValue);
|
||||
const label = definedNames.find((i) => i.value === newValue)?.label;
|
||||
const oldLabel = definedNames.find((i) => i.value === payload)?.label;
|
||||
if (label && (getIsDisplaySyncedWithPayload() || !display || display === oldLabel)) {
|
||||
setNormalizedDisplay(label);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{customHyperLinkSidePanel?.Form && (
|
||||
<customHyperLinkSidePanel.Form
|
||||
linkId={id}
|
||||
payload={payload}
|
||||
display={display}
|
||||
showError={showError}
|
||||
getIsDisplaySyncedWithPayload={getIsDisplaySyncedWithPayload}
|
||||
setDisplaySyncedWithPayload={setDisplaySyncedWithPayload}
|
||||
setDisplay={(newLink) => {
|
||||
setNormalizedDisplay(newLink);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}}
|
||||
setPayload={setPayload}
|
||||
/>
|
||||
)}
|
||||
<ActionRow className="univer-flex univer-flex-row univer-justify-end univer-gap-2">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (editing) {
|
||||
await resolverService.navigateToRange(editing.unitId, editing.subUnitId, { startRow: editing.row, endRow: editing.row, startColumn: editing.col, endColumn: editing.col }, true);
|
||||
}
|
||||
await commandService.executeCommand(CloseHyperLinkPopupOperation.id);
|
||||
}}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-hyper-link-ui.form.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-hyper-link-ui.form.ok')}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CellLinkEdit.componentKey = 'univer.sheet.cell-link-edit';
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { isBlankInput, resolveRangePayload } from '../utils';
|
||||
|
||||
describe('CellLinkEdit utils', () => {
|
||||
it('treats whitespace-only labels as blank input', () => {
|
||||
expect(isBlankInput('')).toBe(true);
|
||||
expect(isBlankInput(' ')).toBe(true);
|
||||
expect(isBlankInput('\n\t')).toBe(true);
|
||||
expect(isBlankInput(' label ')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns an empty payload for invalid range text', () => {
|
||||
expect(resolveRangePayload('', 'Sheet1')).toBe('');
|
||||
expect(resolveRangePayload('not-a-range', 'Sheet1')).toBe('');
|
||||
});
|
||||
|
||||
it('normalizes valid range text and falls back to the active sheet name', () => {
|
||||
expect(resolveRangePayload('A1:B2', 'Sheet1')).toBe('Sheet1!A1:B2');
|
||||
expect(resolveRangePayload('Sheet2!A1:B2', 'Sheet1')).toBe('Sheet2!A1:B2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { isValidRange } from '@univerjs/core';
|
||||
import { deserializeRangeWithSheet, serializeRangeToRefString } from '@univerjs/engine-formula';
|
||||
|
||||
export function isBlankInput(value: string) {
|
||||
return value.trim().length === 0;
|
||||
}
|
||||
|
||||
export function resolveRangePayload(rangeText: string, defaultSheetName: string) {
|
||||
const [range] = rangeText.split(',').map(deserializeRangeWithSheet);
|
||||
|
||||
if (!range || !isValidRange(range.range)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!range.sheetName) {
|
||||
range.sheetName = defaultSheetName;
|
||||
}
|
||||
|
||||
return serializeRangeToRefString(range);
|
||||
}
|
||||
225
packages/sheets-hyper-link-ui/src/views/CellLinkPopup.tsx
Normal file
225
packages/sheets-hyper-link-ui/src/views/CellLinkPopup.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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 { ICustomRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
|
||||
import { borderClassName, Button, clsx, MessageType, Tooltip } from '@univerjs/design';
|
||||
import { AllBorderIcon, CopyIcon, LinkIcon, SheetsMultiIcon, UnlinkIcon, WriteIcon } from '@univerjs/icons';
|
||||
import {
|
||||
CancelHyperLinkCommand,
|
||||
CancelRichHyperLinkCommand,
|
||||
SheetHyperLinkType,
|
||||
SheetsHyperLinkParserService,
|
||||
} from '@univerjs/sheets-hyper-link';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { IMessageService, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { OpenHyperLinkEditPanelOperation } from '../commands/operations/popup.operations';
|
||||
import { ISheetsHyperLinkPopupService } from '../services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from '../services/resolver.service';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
|
||||
const iconsMap = {
|
||||
[SheetHyperLinkType.URL]: <LinkIcon />,
|
||||
[SheetHyperLinkType.SHEET]: <SheetsMultiIcon className="univer-text-green-500" />,
|
||||
[SheetHyperLinkType.RANGE]: <AllBorderIcon />,
|
||||
[SheetHyperLinkType.DEFINE_NAME]: <AllBorderIcon />,
|
||||
[SheetHyperLinkType.INVALID]: <AllBorderIcon />,
|
||||
};
|
||||
|
||||
interface ICellLinkPopupPureProps {
|
||||
customRange?: Nullable<ICustomRange>;
|
||||
row: number;
|
||||
col: number;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
editPermission?: boolean;
|
||||
copyPermission?: boolean;
|
||||
type: HyperLinkEditSourceType;
|
||||
}
|
||||
|
||||
export const CellLinkPopupPure = (props: ICellLinkPopupPureProps) => {
|
||||
const popupService = useDependency(ISheetsHyperLinkPopupService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const messageService = useDependency(IMessageService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const resolverService = useDependency(SheetsHyperLinkResolverService);
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const parserHyperLinkService = useDependency(SheetsHyperLinkParserService);
|
||||
const { customRange, row, col, unitId, subUnitId, editPermission, copyPermission, type } = props;
|
||||
|
||||
if (!customRange?.properties?.url) {
|
||||
return null;
|
||||
}
|
||||
const linkObj = parserHyperLinkService.parseHyperLink(customRange.properties.url ?? '');
|
||||
const isError = linkObj.type === SheetHyperLinkType.INVALID;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-mb-1 univer-flex univer-max-w-80 univer-flex-row univer-items-center univer-justify-between
|
||||
univer-gap-2 univer-overflow-hidden univer-rounded-lg univer-bg-gray-0 univer-p-2 univer-shadow-md
|
||||
dark:!univer-bg-gray-900
|
||||
`, borderClassName)}
|
||||
onClick={() => popupService.hideCurrentPopup()}
|
||||
>
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-flex univer-h-6 univer-flex-1 univer-cursor-pointer univer-flex-row univer-items-center
|
||||
univer-gap-1 univer-truncate univer-text-sm univer-leading-5 univer-text-primary-600
|
||||
`, { 'univer-text-red-500': isError })}
|
||||
onClick={() => {
|
||||
if (isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolverService.navigate(linkObj);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-size-5 univer-flex-none univer-items-center univer-justify-center
|
||||
univer-text-base univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
>
|
||||
{iconsMap[linkObj.type]}
|
||||
</div>
|
||||
<Tooltip showIfEllipsis title={linkObj.name} asChild>
|
||||
<span className="univer-flex-1 univer-truncate">{linkObj.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-h-6 univer-flex-none univer-flex-row univer-items-center univer-justify-center
|
||||
univer-gap-2
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
size="small"
|
||||
data-u-comp="cell-link-popup-copy"
|
||||
aria-label={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.copy')}
|
||||
className={clsx(`
|
||||
!univer-flex univer-size-6 !univer-rounded !univer-border-0 !univer-p-0 !univer-text-base
|
||||
!univer-font-normal
|
||||
disabled:!univer-pointer-events-auto
|
||||
`, { 'univer-text-red-500': isError })}
|
||||
disabled={!copyPermission || isError}
|
||||
onClick={async () => {
|
||||
if (linkObj.type !== SheetHyperLinkType.URL) {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = linkObj.url.slice(1);
|
||||
await navigator.clipboard.writeText(url.href);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(linkObj.url);
|
||||
}
|
||||
messageService.show({
|
||||
content: localeService.t<LocaleKey>('sheets-hyper-link-ui.message.coped'),
|
||||
type: MessageType.Info,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="bottom" title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.copy')}>
|
||||
<CopyIcon className="dark:!univer-text-gray-0" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
{editPermission && (
|
||||
<>
|
||||
<div
|
||||
data-u-comp="cell-link-popup-edit"
|
||||
className={`
|
||||
univer-flex univer-size-6 univer-cursor-pointer univer-flex-row univer-items-center
|
||||
univer-justify-center univer-rounded univer-text-base
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`}
|
||||
onClick={() => {
|
||||
return commandService.executeCommand(OpenHyperLinkEditPanelOperation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
row,
|
||||
col,
|
||||
customRangeId: customRange.rangeId,
|
||||
type,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="bottom" title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.edit')}>
|
||||
<WriteIcon className="dark:!univer-text-gray-0" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
data-u-comp="cell-link-popup-remove"
|
||||
className={`
|
||||
univer-flex univer-size-6 univer-cursor-pointer univer-flex-row univer-items-center
|
||||
univer-justify-center univer-rounded univer-text-base
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`}
|
||||
onClick={() => {
|
||||
const commandId = type === HyperLinkEditSourceType.EDITING ? CancelRichHyperLinkCommand.id : CancelHyperLinkCommand.id;
|
||||
if (commandService.syncExecuteCommand(commandId, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id: customRange.rangeId,
|
||||
row,
|
||||
column: col,
|
||||
documentId: editorBridgeService.getCurrentEditorId(),
|
||||
})) {
|
||||
popupService.hideCurrentPopup(undefined, true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="bottom" title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.cancel')}>
|
||||
<UnlinkIcon className="dark:!univer-text-gray-0" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CellLinkPopup = () => {
|
||||
const popupService = useDependency(ISheetsHyperLinkPopupService);
|
||||
const currentPopup = useObservable(popupService.currentPopup$, popupService.currentPopup);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
if (!currentPopup) {
|
||||
return null;
|
||||
}
|
||||
if (currentPopup.showAll) {
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(currentPopup.unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(currentPopup.subUnitId);
|
||||
const cell = worksheet?.getCell(currentPopup.row, currentPopup.col);
|
||||
const customRanges = cell?.p?.body?.customRanges;
|
||||
return customRanges?.length
|
||||
? (
|
||||
<div>
|
||||
{customRanges.map((customRange) => {
|
||||
return <CellLinkPopupPure key={customRange.rangeId} {...currentPopup} customRange={customRange} />;
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
return <CellLinkPopupPure {...currentPopup} />;
|
||||
};
|
||||
|
||||
CellLinkPopup.componentKey = 'univer.sheet.cell-link-popup';
|
||||
589
packages/sheets-hyper-link-ui/src/views/MobileCellLinkEdit.tsx
Normal file
589
packages/sheets-hyper-link-ui/src/views/MobileCellLinkEdit.tsx
Normal file
@@ -0,0 +1,589 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
BuildTextUtils,
|
||||
ColorKit,
|
||||
CustomRangeType,
|
||||
DataStreamTreeTokenType,
|
||||
DisposableCollection,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
ThemeService,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { Button, FormLayout, Input, MobileActionRowGroup, MobileSelect } from '@univerjs/design';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import {
|
||||
deserializeRangeWithSheet,
|
||||
IDefinedNamesService,
|
||||
serializeRange,
|
||||
serializeRangeWithSheet,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { MobileRangeSelector } from '@univerjs/sheets-formula-ui';
|
||||
import {
|
||||
AddHyperLinkCommand,
|
||||
AddRichHyperLinkCommand,
|
||||
SheetHyperLinkType,
|
||||
SheetsHyperLinkParserService,
|
||||
UpdateHyperLinkCommand,
|
||||
UpdateRichHyperLinkCommand,
|
||||
} from '@univerjs/sheets-hyper-link';
|
||||
import { IEditorBridgeService, IMarkSelectionService, ScrollToRangeOperation } from '@univerjs/sheets-ui';
|
||||
import { KeyCode, useDependency, useEvent, useObservable } from '@univerjs/ui';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CloseHyperLinkPopupOperation } from '../commands/operations/popup.operations';
|
||||
import { isLegalLink, serializeUrl } from '../common/util';
|
||||
import { ISheetsHyperLinkPopupService } from '../services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from '../services/resolver.service';
|
||||
import { SheetsHyperLinkSidePanelService } from '../services/side-panel.service';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
import { isBlankInput, resolveRangePayload } from './CellLinkEdit/utils';
|
||||
|
||||
export const MobileCellLinkEdit = () => {
|
||||
const [id, setId] = useState('');
|
||||
const [display, setDisplay] = useState('');
|
||||
const [showLabel, setShowLabel] = useState(true);
|
||||
const [type, setType] = useState<SheetHyperLinkType | string>(SheetHyperLinkType.URL);
|
||||
const [payload, setPayload] = useState('');
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const definedNameService = useDependency(IDefinedNamesService);
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const popupService = useDependency(ISheetsHyperLinkPopupService);
|
||||
const editing = useObservable(popupService.currentEditing$);
|
||||
const parserService = useDependency(SheetsHyperLinkParserService);
|
||||
const resolverService = useDependency(SheetsHyperLinkResolverService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const sidePanelService = useDependency(SheetsHyperLinkSidePanelService);
|
||||
const sidePanelOptions = useMemo(() => sidePanelService.getOptions(), [sidePanelService]);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const markSelectionService = useDependency(IMarkSelectionService);
|
||||
const textSelectionService = useDependency(DocSelectionManagerService);
|
||||
const themeService = useDependency(ThemeService);
|
||||
const [selectorDialogVisible, setSelectorDialogVisible] = useState(false);
|
||||
const sheetsSelectionService = useDependency(SheetsSelectionsService);
|
||||
const selections = useMemo(() => sheetsSelectionService.getCurrentSelections(), []);
|
||||
|
||||
const customHyperLinkSidePanel = useMemo(() => {
|
||||
if (sidePanelService.isBuiltInLinkType(type)) {
|
||||
return;
|
||||
}
|
||||
return sidePanelService.getCustomHyperLink(type);
|
||||
}, [sidePanelService, type]);
|
||||
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
const [isFocusRangeSelector, setIsFocusRangeSelector] = useState(false);
|
||||
|
||||
const isDisplaySyncedWithPayloadRef = useRef(false);
|
||||
const getIsDisplaySyncedWithPayload = useEvent(() => isDisplaySyncedWithPayloadRef.current);
|
||||
const setDisplaySyncedWithPayload = useEvent((synced: boolean) => {
|
||||
isDisplaySyncedWithPayloadRef.current = synced;
|
||||
});
|
||||
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
|
||||
const subUnitId = workbook?.getActiveSheet().getSheetId() || '';
|
||||
// to polyfill the display value on old version data
|
||||
// case split tag is no longer needed
|
||||
const setNormalizedDisplay = useCallback((value: string) => {
|
||||
setDisplay(value.replaceAll(DataStreamTreeTokenType.CUSTOM_RANGE_START, '').replaceAll(DataStreamTreeTokenType.CUSTOM_RANGE_END, ''));
|
||||
}, [setDisplay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing?.row !== undefined && editing.col !== undefined) {
|
||||
const { customRange, row, col } = editing;
|
||||
let { label } = editing;
|
||||
if (typeof label === 'number') {
|
||||
label = `${label}`;
|
||||
}
|
||||
|
||||
let link;
|
||||
if (customRange) {
|
||||
link = {
|
||||
id: customRange?.rangeId ?? '',
|
||||
display: label ?? '',
|
||||
payload: customRange?.properties?.url ?? '',
|
||||
row,
|
||||
column: col,
|
||||
};
|
||||
} else {
|
||||
if (editing.type === HyperLinkEditSourceType.VIEWING) {
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(editing.unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(editing.subUnitId);
|
||||
const cell = worksheet?.getCellRaw(editing.row, editing.col);
|
||||
const range = cell?.p?.body?.customRanges?.find((range) => range.rangeType === CustomRangeType.HYPERLINK && range.properties?.url);
|
||||
const cellValue = cell?.v;
|
||||
if (cell && (!BuildTextUtils.transform.isEmptyDocument(cell.p?.body?.dataStream) || Tools.isDefine(cellValue))) {
|
||||
setShowLabel(false);
|
||||
}
|
||||
link = {
|
||||
id: '',
|
||||
display: '',
|
||||
payload: range?.properties?.url ?? '',
|
||||
row,
|
||||
column: col,
|
||||
};
|
||||
} else {
|
||||
const doc = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const currentSelection = textSelectionService.getActiveTextRange();
|
||||
const body = doc?.getBody();
|
||||
const selection = currentSelection && body ? currentSelection : null;
|
||||
const customRange = selection && BuildTextUtils.customRange.getCustomRangesInterestsWithSelection(selection, body?.customRanges ?? [])?.[0];
|
||||
|
||||
setShowLabel(false);
|
||||
link = {
|
||||
id: '',
|
||||
display: label ?? '',
|
||||
payload: customRange?.properties?.url ?? '',
|
||||
row,
|
||||
column: col,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setId(link.id);
|
||||
const customLink = sidePanelService.findCustomHyperLink(link);
|
||||
if (customLink) {
|
||||
const customLinkInfo = customLink.convert(link);
|
||||
setType(customLinkInfo.type);
|
||||
setPayload(customLinkInfo.payload);
|
||||
setNormalizedDisplay(customLinkInfo.display);
|
||||
return;
|
||||
}
|
||||
setNormalizedDisplay(link.display);
|
||||
const linkInfo = parserService.parseHyperLink(link.payload);
|
||||
setType(linkInfo.type === SheetHyperLinkType.INVALID ? SheetHyperLinkType.RANGE : linkInfo.type);
|
||||
switch (linkInfo.type) {
|
||||
case SheetHyperLinkType.URL: {
|
||||
setPayload(linkInfo.url);
|
||||
if (linkInfo.url === link.display) {
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SheetHyperLinkType.RANGE: {
|
||||
const params = linkInfo.searchObj!;
|
||||
const sheetName = params.gid ?
|
||||
univerInstanceService
|
||||
.getUnit<Workbook>(editing.unitId)
|
||||
?.getSheetBySheetId(params.gid)
|
||||
?.getName()
|
||||
?? ''
|
||||
: '';
|
||||
const payload = (serializeRangeWithSheet(sheetName, deserializeRangeWithSheet(params.range!).range));
|
||||
setPayload(payload);
|
||||
if (payload === link.display) {
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SheetHyperLinkType.SHEET: {
|
||||
const params = linkInfo.searchObj!;
|
||||
setPayload(params.gid!);
|
||||
break;
|
||||
}
|
||||
case SheetHyperLinkType.DEFINE_NAME: {
|
||||
const params = linkInfo.searchObj!;
|
||||
setPayload(params.rangeid!);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
setPayload('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [editing, resolverService, sidePanelService, textSelectionService, univerInstanceService]);
|
||||
|
||||
useEffect(() => {
|
||||
let id: Nullable<string> = null;
|
||||
if (editing && !editing.customRangeId && editing.type === HyperLinkEditSourceType.VIEWING && Tools.isDefine(editing.row) && Tools.isDefine(editing.col)) {
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(editing.unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(editing.subUnitId);
|
||||
const mergeInfo = worksheet?.getMergedCell(editing.row, editing.col);
|
||||
const color = new ColorKit(themeService.getColorFromTheme('primary.600')).toRgb();
|
||||
id = markSelectionService.addShape(
|
||||
{
|
||||
range: mergeInfo ?? {
|
||||
startColumn: editing.col,
|
||||
endColumn: editing.col,
|
||||
startRow: editing.row,
|
||||
endRow: editing.row,
|
||||
},
|
||||
style: {
|
||||
// hasAutoFill: false,
|
||||
fill: `rgb(${color.r}, ${color.g}, ${color.b}, 0.12)`,
|
||||
strokeWidth: 1,
|
||||
stroke: '#FFBD37',
|
||||
widgets: {},
|
||||
},
|
||||
primary: null,
|
||||
},
|
||||
[],
|
||||
-1
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
if (id) {
|
||||
markSelectionService.removeShape(id);
|
||||
}
|
||||
};
|
||||
}, [editing, markSelectionService, themeService, univerInstanceService]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsFocusRangeSelector(type === SheetHyperLinkType.RANGE);
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
const render = renderManagerService.getRenderUnitById(editorBridgeService.getCurrentEditorId());
|
||||
const disposeCollection = new DisposableCollection();
|
||||
|
||||
if (render) {
|
||||
const selectionRenderService = render.with(DocSelectionRenderService);
|
||||
selectionRenderService.setReserveRangesStatus(true);
|
||||
disposeCollection.add(() => {
|
||||
selectionRenderService.setReserveRangesStatus(false);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
editorBridgeService.disableForceKeepVisible();
|
||||
disposeCollection.dispose();
|
||||
};
|
||||
}, [editorBridgeService, renderManagerService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusRangeSelector) {
|
||||
popupService.setIsKeepVisible(isFocusRangeSelector);
|
||||
}
|
||||
popupService.setIsKeepVisible(selectorDialogVisible);
|
||||
|
||||
return () => {
|
||||
popupService.setIsKeepVisible(false);
|
||||
};
|
||||
}, [isFocusRangeSelector, selectorDialogVisible, popupService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusRangeSelector) {
|
||||
editorBridgeService.enableForceKeepVisible();
|
||||
|
||||
return () => {
|
||||
editorBridgeService.disableForceKeepVisible();
|
||||
};
|
||||
}
|
||||
}, [isFocusRangeSelector, editorBridgeService]);
|
||||
|
||||
const linkTypeOptions: Array<{
|
||||
label: string;
|
||||
value: SheetHyperLinkType | string;
|
||||
}> = [
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.link'),
|
||||
value: SheetHyperLinkType.URL,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.range'),
|
||||
value: SheetHyperLinkType.RANGE,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.worksheet'),
|
||||
value: SheetHyperLinkType.SHEET,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('sheets-hyper-link-ui.form.definedName'),
|
||||
value: SheetHyperLinkType.DEFINE_NAME,
|
||||
},
|
||||
...sidePanelOptions,
|
||||
];
|
||||
|
||||
const handleRangeChange = useEvent((rangeText: string) => {
|
||||
const newPayload = resolveRangePayload(rangeText, workbook?.getActiveSheet()?.getName() || '');
|
||||
if (!newPayload) {
|
||||
setPayload('');
|
||||
return;
|
||||
}
|
||||
|
||||
setPayload(newPayload);
|
||||
|
||||
if (getIsDisplaySyncedWithPayload() || !display) {
|
||||
setNormalizedDisplay(newPayload);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hiddens = workbook.getHiddenWorksheets();
|
||||
const sheetsOption = workbook.getSheets().map((sheet) => ({ label: sheet.getName(), value: sheet.getSheetId() })).filter((opt) => hiddens.indexOf(opt.value) === -1);
|
||||
const definedNames = Object.values(definedNameService.getDefinedNameMap(workbook.getUnitId()) ?? {}).map((value) => ({
|
||||
label: value.name,
|
||||
value: value.id,
|
||||
}));
|
||||
|
||||
const formatUrl = (type: SheetHyperLinkType | string, payload: string) => {
|
||||
if (type === SheetHyperLinkType.URL) {
|
||||
return serializeUrl(payload);
|
||||
}
|
||||
|
||||
if (type === SheetHyperLinkType.RANGE) {
|
||||
const info = deserializeRangeWithSheet(payload);
|
||||
const worksheet = workbook.getSheetBySheetName(info.sheetName);
|
||||
if (worksheet) {
|
||||
return `#gid=${worksheet.getSheetId()}&range=${serializeRange(info.range)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `#${type}=${payload}`;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if ((showLabel && isBlankInput(display)) || !payload || (type === SheetHyperLinkType.URL && !isLegalLink(payload))) {
|
||||
setShowError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
if (id) {
|
||||
const commandId = editing.type === HyperLinkEditSourceType.EDITING ? UpdateRichHyperLinkCommand.id : UpdateHyperLinkCommand.id;
|
||||
await commandService.executeCommand(commandId, {
|
||||
id,
|
||||
unitId: editing.unitId,
|
||||
subUnitId: editing.subUnitId,
|
||||
payload: {
|
||||
display: showLabel ? display : '',
|
||||
payload: formatUrl(type, payload),
|
||||
},
|
||||
row: editing.row,
|
||||
column: editing.col,
|
||||
documentId: editorBridgeService.getCurrentEditorId(),
|
||||
});
|
||||
} else {
|
||||
const commandId = editing.type === HyperLinkEditSourceType.EDITING ? AddRichHyperLinkCommand.id : AddHyperLinkCommand.id;
|
||||
await commandService.executeCommand(commandId, {
|
||||
unitId: editing.unitId,
|
||||
subUnitId: editing.subUnitId,
|
||||
link: {
|
||||
id: generateRandomId(),
|
||||
row: editing.row,
|
||||
column: editing.col,
|
||||
payload: formatUrl(type, payload),
|
||||
display: showLabel ? display : '',
|
||||
},
|
||||
documentId: editorBridgeService.getCurrentEditorId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (editing?.type === HyperLinkEditSourceType.VIEWING) {
|
||||
await commandService.executeCommand(SetWorksheetActiveOperation.id, {
|
||||
unitId: editing.unitId,
|
||||
subUnitId: editing.subUnitId,
|
||||
});
|
||||
|
||||
const GAP = 1;
|
||||
await commandService.executeCommand(ScrollToRangeOperation.id, {
|
||||
range: {
|
||||
startRow: Math.max(editing.row - GAP, 0),
|
||||
endRow: editing.row + GAP,
|
||||
startColumn: Math.max(editing.col - GAP, 0),
|
||||
endColumn: editing.col + GAP,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await commandService.executeCommand(CloseHyperLinkPopupOperation.id);
|
||||
};
|
||||
|
||||
if (!editing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
univer-box-border univer-w-full univer-bg-gray-0 univer-p-0
|
||||
dark:!univer-bg-gray-900
|
||||
"
|
||||
>
|
||||
{showLabel
|
||||
? (
|
||||
<FormLayout
|
||||
label={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.label')}
|
||||
error={showError && isBlankInput(display) ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.inputError') : ''}
|
||||
>
|
||||
<Input
|
||||
value={display}
|
||||
onChange={(v) => {
|
||||
setNormalizedDisplay(v);
|
||||
setDisplaySyncedWithPayload(false);
|
||||
}}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.labelPlaceholder')}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)
|
||||
: null}
|
||||
<FormLayout label={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.type')}>
|
||||
<MobileSelect
|
||||
className="univer-w-full"
|
||||
options={linkTypeOptions}
|
||||
value={type}
|
||||
onChange={(newType) => {
|
||||
setType(newType as SheetHyperLinkType);
|
||||
setPayload('');
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
{type === SheetHyperLinkType.URL && (
|
||||
<FormLayout
|
||||
error={showError ? !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.inputError') : !isLegalLink(payload) ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.linkError') : '' : ''}
|
||||
>
|
||||
<Input
|
||||
dir="ltr"
|
||||
value={payload}
|
||||
onChange={(newLink) => {
|
||||
setPayload(newLink);
|
||||
if (newLink && (getIsDisplaySyncedWithPayload() || !display || display === newLink)) {
|
||||
setNormalizedDisplay(newLink);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
}}
|
||||
placeholder={localeService.t<LocaleKey>('sheets-hyper-link-ui.form.linkPlaceholder')}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === KeyCode.ENTER) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{type === SheetHyperLinkType.RANGE && (
|
||||
<FormLayout error={showError && !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.inputError') : ''}>
|
||||
<MobileRangeSelector
|
||||
unitId={workbook.getUnitId()}
|
||||
subUnitId={subUnitId}
|
||||
maxRangeCount={1}
|
||||
supportAcrossSheet
|
||||
initialValue={payload}
|
||||
resetRange={selections as ISelectionWithStyle[]}
|
||||
onChange={(_, text) => handleRangeChange(text)}
|
||||
onRangeSelectorDialogVisibleChange={async (visible) => {
|
||||
setSelectorDialogVisible(visible);
|
||||
if (visible) {
|
||||
if (editing.type !== HyperLinkEditSourceType.VIEWING) {
|
||||
editorBridgeService.enableForceKeepVisible();
|
||||
}
|
||||
} else {
|
||||
await resolverService.navigateToRange(editing.unitId, editing.subUnitId, { startRow: editing.row, endRow: editing.row, startColumn: editing.col, endColumn: editing.col }, true);
|
||||
editorBridgeService.disableForceKeepVisible();
|
||||
}
|
||||
}}
|
||||
onFocusChange={(focus) => setIsFocusRangeSelector(focus)}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{type === SheetHyperLinkType.SHEET && (
|
||||
<FormLayout error={showError && !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.selectError') : ''}>
|
||||
<MobileSelect
|
||||
className="univer-w-full"
|
||||
options={sheetsOption}
|
||||
value={payload}
|
||||
onChange={(newPayload) => {
|
||||
setPayload(newPayload);
|
||||
const label = sheetsOption.find((i) => i.value === newPayload)?.label;
|
||||
const oldLabel = sheetsOption.find((i) => i.value === payload)?.label;
|
||||
if (label && (getIsDisplaySyncedWithPayload() || !display || display === oldLabel)) {
|
||||
setNormalizedDisplay(label);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{type === SheetHyperLinkType.DEFINE_NAME && (
|
||||
<FormLayout error={showError && !payload ? localeService.t<LocaleKey>('sheets-hyper-link-ui.form.selectError') : ''}>
|
||||
<MobileSelect
|
||||
className="univer-w-full"
|
||||
options={definedNames}
|
||||
value={payload}
|
||||
onChange={(newValue) => {
|
||||
setPayload(newValue);
|
||||
const label = definedNames.find((i) => i.value === newValue)?.label;
|
||||
const oldLabel = definedNames.find((i) => i.value === payload)?.label;
|
||||
if (label && (getIsDisplaySyncedWithPayload() || !display || display === oldLabel)) {
|
||||
setNormalizedDisplay(label);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
{customHyperLinkSidePanel?.Form && (
|
||||
<customHyperLinkSidePanel.Form
|
||||
linkId={id}
|
||||
payload={payload}
|
||||
display={display}
|
||||
showError={showError}
|
||||
getIsDisplaySyncedWithPayload={getIsDisplaySyncedWithPayload}
|
||||
setDisplaySyncedWithPayload={setDisplaySyncedWithPayload}
|
||||
setDisplay={(newLink) => {
|
||||
setNormalizedDisplay(newLink);
|
||||
setDisplaySyncedWithPayload(true);
|
||||
}}
|
||||
setPayload={setPayload}
|
||||
/>
|
||||
)}
|
||||
<MobileActionRowGroup className="univer-mt-5">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (editing) {
|
||||
await resolverService.navigateToRange(editing.unitId, editing.subUnitId, { startRow: editing.row, endRow: editing.row, startColumn: editing.col, endColumn: editing.col }, true);
|
||||
}
|
||||
await commandService.executeCommand(CloseHyperLinkPopupOperation.id);
|
||||
}}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-hyper-link-ui.form.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{localeService.t<LocaleKey>('sheets-hyper-link-ui.form.ok')}
|
||||
</Button>
|
||||
</MobileActionRowGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MobileCellLinkEdit.componentKey = 'univer.sheet.mobile-cell-link-edit';
|
||||
178
packages/sheets-hyper-link-ui/src/views/MobileCellLinkPopup.tsx
Normal file
178
packages/sheets-hyper-link-ui/src/views/MobileCellLinkPopup.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 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 { ICustomRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
|
||||
import { MessageType, MobileActionRow } from '@univerjs/design';
|
||||
import {
|
||||
CancelHyperLinkCommand,
|
||||
CancelRichHyperLinkCommand,
|
||||
SheetHyperLinkType,
|
||||
SheetsHyperLinkParserService,
|
||||
} from '@univerjs/sheets-hyper-link';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { IMessageService, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { OpenHyperLinkEditPanelOperation } from '../commands/operations/popup.operations';
|
||||
import { ISheetsHyperLinkPopupService } from '../services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from '../services/resolver.service';
|
||||
import { HyperLinkEditSourceType } from '../types/enums/edit-source';
|
||||
|
||||
interface IMobileCellLinkPopupPureProps {
|
||||
customRange?: Nullable<ICustomRange>;
|
||||
row: number;
|
||||
col: number;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
editPermission?: boolean;
|
||||
copyPermission?: boolean;
|
||||
type: HyperLinkEditSourceType;
|
||||
}
|
||||
|
||||
export function MobileCellLinkPopupPure(props: IMobileCellLinkPopupPureProps) {
|
||||
const popupService = useDependency(ISheetsHyperLinkPopupService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const messageService = useDependency(IMessageService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const resolverService = useDependency(SheetsHyperLinkResolverService);
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const parserHyperLinkService = useDependency(SheetsHyperLinkParserService);
|
||||
const { customRange, row, col, unitId, subUnitId, editPermission, copyPermission, type } = props;
|
||||
|
||||
if (!customRange?.properties?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const linkObj = parserHyperLinkService.parseHyperLink(customRange.properties.url);
|
||||
const isError = linkObj.type === SheetHyperLinkType.INVALID;
|
||||
const close = () => popupService.hideCurrentPopup(undefined, true);
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-flex-col univer-gap-2">
|
||||
<MobileActionRow
|
||||
title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.open')}
|
||||
value={linkObj.name}
|
||||
valueType="text"
|
||||
aria-label={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.open')}
|
||||
variant="subtle"
|
||||
disabled={isError}
|
||||
onClick={() => {
|
||||
resolverService.navigate(linkObj);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{copyPermission && (
|
||||
<MobileActionRow
|
||||
title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.copy')}
|
||||
aria-label={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.copy')}
|
||||
variant="subtle"
|
||||
disabled={isError}
|
||||
onClick={() => {
|
||||
let url = linkObj.url;
|
||||
if (linkObj.type !== SheetHyperLinkType.URL) {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.hash = linkObj.url.slice(1);
|
||||
url = currentUrl.href;
|
||||
}
|
||||
|
||||
return navigator.clipboard.writeText(url).then(() => {
|
||||
messageService.show({
|
||||
content: localeService.t<LocaleKey>('sheets-hyper-link-ui.message.coped'),
|
||||
type: MessageType.Info,
|
||||
});
|
||||
close();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{editPermission && (
|
||||
<>
|
||||
<MobileActionRow
|
||||
title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.edit')}
|
||||
aria-label={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.edit')}
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
close();
|
||||
return commandService.executeCommand(OpenHyperLinkEditPanelOperation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
row,
|
||||
col,
|
||||
customRangeId: customRange.rangeId,
|
||||
type,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<MobileActionRow
|
||||
title={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.cancel')}
|
||||
aria-label={localeService.t<LocaleKey>('sheets-hyper-link-ui.popup.cancel')}
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
const commandId = type === HyperLinkEditSourceType.EDITING
|
||||
? CancelRichHyperLinkCommand.id
|
||||
: CancelHyperLinkCommand.id;
|
||||
if (commandService.syncExecuteCommand(commandId, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id: customRange.rangeId,
|
||||
row,
|
||||
column: col,
|
||||
documentId: editorBridgeService.getCurrentEditorId(),
|
||||
})) {
|
||||
close();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileCellLinkPopup() {
|
||||
const popupService = useDependency(ISheetsHyperLinkPopupService);
|
||||
const currentPopup = useObservable(popupService.currentPopup$, popupService.currentPopup);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
if (!currentPopup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentPopup.showAll) {
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(currentPopup.unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(currentPopup.subUnitId);
|
||||
const cell = worksheet?.getCell(currentPopup.row, currentPopup.col);
|
||||
const customRanges = cell?.p?.body?.customRanges;
|
||||
|
||||
return customRanges?.length
|
||||
? (
|
||||
<div>
|
||||
{customRanges.map((customRange) => (
|
||||
<MobileCellLinkPopupPure
|
||||
key={customRange.rangeId}
|
||||
{...currentPopup}
|
||||
customRange={customRange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
return <MobileCellLinkPopupPure {...currentPopup} />;
|
||||
}
|
||||
|
||||
MobileCellLinkPopup.componentKey = 'univer.sheet.mobile-cell-link-popup';
|
||||
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 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 { ICustomRange, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
CellValueType,
|
||||
CustomRangeType,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
toDisposable,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { IDefinedNamesService } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
SetRangeValuesMutation,
|
||||
SetWorksheetActiveOperation,
|
||||
SheetInterceptorService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
AddHyperLinkCommand,
|
||||
AddHyperLinkMutation,
|
||||
CancelHyperLinkCommand,
|
||||
HyperLinkModel,
|
||||
RemoveHyperLinkMutation,
|
||||
SheetHyperLinkType,
|
||||
SheetsHyperLinkParserService,
|
||||
UpdateHyperLinkMutation,
|
||||
} from '@univerjs/sheets-hyper-link';
|
||||
import {
|
||||
IEditorBridgeService,
|
||||
IMarkSelectionService,
|
||||
ScrollToRangeOperation,
|
||||
SheetCanvasPopManagerService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import { IDialogService, IMessageService, RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { of } from 'rxjs';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { CloseHyperLinkPopupOperation, OpenHyperLinkEditPanelOperation } from '../../commands/operations/popup.operations';
|
||||
import { ISheetsHyperLinkPopupService, SheetsHyperLinkPopupService } from '../../services/popup.service';
|
||||
import { SheetsHyperLinkResolverService } from '../../services/resolver.service';
|
||||
import { SheetsHyperLinkSidePanelService } from '../../services/side-panel.service';
|
||||
import { HyperLinkEditSourceType } from '../../types/enums/edit-source';
|
||||
import { CellLinkEdit } from '../CellLinkEdit';
|
||||
import { CellLinkPopupPure } from '../CellLinkPopup';
|
||||
import { MobileCellLinkEdit } from '../MobileCellLinkEdit';
|
||||
import { MobileCellLinkPopupPure } from '../MobileCellLinkPopup';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'sheet-link-popup-workbook';
|
||||
const SUB_UNIT_ID = 'sheet-1';
|
||||
|
||||
class TestCanvasPopManagerService {
|
||||
attachPopupToCell() {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
attachPopupByPosition() {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
attachPopupToAbsolutePosition() {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
enableScrollRender = false;
|
||||
readonly scrollRanges: unknown[] = [];
|
||||
|
||||
getRenderUnitById(id: string) {
|
||||
if (id !== UNIT_ID || !this.enableScrollRender) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
with: () => ({
|
||||
scrollToRange: (range: unknown) => {
|
||||
this.scrollRanges.push(range);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestMarkSelectionService {
|
||||
readonly shapes = new Map<string, unknown>();
|
||||
|
||||
addShape(selection: unknown) {
|
||||
const id = `shape-${this.shapes.size + 1}`;
|
||||
this.shapes.set(id, selection);
|
||||
return id;
|
||||
}
|
||||
|
||||
addShapeWithNoFresh(selection: unknown) {
|
||||
return this.addShape(selection);
|
||||
}
|
||||
|
||||
removeShape(id: string) {
|
||||
this.shapes.delete(id);
|
||||
}
|
||||
|
||||
removeAllShapes() {
|
||||
this.shapes.clear();
|
||||
}
|
||||
|
||||
refreshShapes() {}
|
||||
|
||||
getShapeMap() {
|
||||
return this.shapes;
|
||||
}
|
||||
}
|
||||
|
||||
class TestEditorBridgeService {
|
||||
forceKeepVisible = false;
|
||||
|
||||
getCurrentEditorId() {
|
||||
return 'sheet-popup-editor';
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return { visible: false };
|
||||
}
|
||||
|
||||
getEditCellState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
enableForceKeepVisible() {
|
||||
this.forceKeepVisible = true;
|
||||
}
|
||||
|
||||
disableForceKeepVisible() {
|
||||
this.forceKeepVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDefinedNamesService {
|
||||
getValueById() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getDefinedNameMap() {
|
||||
return {};
|
||||
}
|
||||
|
||||
getWorksheetByRef() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
focusRange() {
|
||||
// Defined-name navigation is outside these popup component scenarios.
|
||||
}
|
||||
}
|
||||
|
||||
class TestMessageService {
|
||||
readonly messages: unknown[] = [];
|
||||
|
||||
show(message: unknown) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
class TestDialogService {
|
||||
open() {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
close(): void {}
|
||||
|
||||
closeAll(): void {}
|
||||
|
||||
getDialogs$() {
|
||||
return of([]);
|
||||
}
|
||||
}
|
||||
|
||||
function createCellDocument(text: string, range: ICustomRange) {
|
||||
return {
|
||||
id: `${range.rangeId}-doc`,
|
||||
documentStyle: {
|
||||
pageSize: { width: 100, height: 40 },
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
body: {
|
||||
dataStream: `${text}\r\n`,
|
||||
customRanges: [range],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkbookData(customRange: ICustomRange): IWorkbookData {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
appVersion: '3.0.0-alpha',
|
||||
name: 'hyperlink popup workbook',
|
||||
locale: LocaleType.EN_US,
|
||||
sheetOrder: [SUB_UNIT_ID],
|
||||
styles: {},
|
||||
sheets: {
|
||||
[SUB_UNIT_ID]: {
|
||||
id: SUB_UNIT_ID,
|
||||
name: 'Sheet 1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
p: createCellDocument('Univer', customRange),
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHyperLinkRange(): ICustomRange {
|
||||
return {
|
||||
rangeId: 'link-range',
|
||||
rangeType: CustomRangeType.HYPERLINK,
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
properties: {
|
||||
url: 'https://univer.ai',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPopupTestBed() {
|
||||
const customRange = createHyperLinkRange();
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([HyperLinkModel]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([IDefinedNamesService, { useClass: TestDefinedNamesService as never }]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([IMessageService, { useClass: TestMessageService as never }]);
|
||||
injector.add([IDialogService, { useClass: TestDialogService }]);
|
||||
injector.add([IEditorBridgeService, { useClass: TestEditorBridgeService as never }]);
|
||||
injector.add([IMarkSelectionService, { useClass: TestMarkSelectionService as never }]);
|
||||
injector.add([SheetCanvasPopManagerService, { useClass: TestCanvasPopManagerService as never }]);
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetsHyperLinkParserService]);
|
||||
injector.add([SheetsHyperLinkResolverService]);
|
||||
injector.add([ISheetsHyperLinkPopupService, { useClass: SheetsHyperLinkPopupService }]);
|
||||
injector.add([SheetsHyperLinkSidePanelService]);
|
||||
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData(customRange));
|
||||
injector.get(IUniverInstanceService).focusUnit(UNIT_ID);
|
||||
injector.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: {
|
||||
'sheets-hyper-link-ui': {
|
||||
popup: {
|
||||
open: 'Open link',
|
||||
edit: 'Edit',
|
||||
cancel: 'Remove link',
|
||||
},
|
||||
form: {
|
||||
label: 'Display text',
|
||||
labelPlaceholder: 'Text',
|
||||
type: 'Link type',
|
||||
link: 'Web link',
|
||||
range: 'Range',
|
||||
worksheet: 'Worksheet',
|
||||
definedName: 'Defined name',
|
||||
linkPlaceholder: 'URL',
|
||||
inputError: 'Required',
|
||||
linkError: 'Invalid link',
|
||||
selectError: 'Select a target',
|
||||
cancel: 'Cancel',
|
||||
ok: 'OK',
|
||||
},
|
||||
message: {
|
||||
coped: 'Copied',
|
||||
},
|
||||
},
|
||||
'sheets-hyper-link': {
|
||||
message: {
|
||||
refError: 'Invalid reference',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(AddHyperLinkMutation);
|
||||
commandService.registerCommand(UpdateHyperLinkMutation);
|
||||
commandService.registerCommand(RemoveHyperLinkMutation);
|
||||
commandService.registerCommand(AddHyperLinkCommand);
|
||||
commandService.registerCommand(CancelHyperLinkCommand);
|
||||
commandService.registerCommand(OpenHyperLinkEditPanelOperation);
|
||||
commandService.registerCommand(CloseHyperLinkPopupOperation);
|
||||
commandService.registerCommand(SetWorksheetActiveOperation);
|
||||
commandService.registerCommand(ScrollToRangeOperation);
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
commandService,
|
||||
workbook,
|
||||
customRange,
|
||||
};
|
||||
}
|
||||
|
||||
function inputText(input: HTMLInputElement, value: string) {
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
valueSetter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
async function selectOption(trigger: Element, optionText: string) {
|
||||
trigger.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
|
||||
trigger.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
|
||||
const option = Array.from(document.querySelectorAll('button, [data-slot="dropdown-menu-radio-item"]')).find((item) => item.textContent === optionText);
|
||||
|
||||
if (!option) {
|
||||
throw new Error(`Option ${optionText} was not rendered`);
|
||||
}
|
||||
|
||||
option.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
}
|
||||
|
||||
function renderPopup(
|
||||
root: Root,
|
||||
container: HTMLDivElement,
|
||||
testBed: ReturnType<typeof createPopupTestBed>
|
||||
) {
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<CellLinkPopupPure
|
||||
unitId={UNIT_ID}
|
||||
subUnitId={SUB_UNIT_ID}
|
||||
row={0}
|
||||
col={0}
|
||||
customRange={testBed.customRange}
|
||||
type={HyperLinkEditSourceType.VIEWING}
|
||||
editPermission
|
||||
copyPermission={false}
|
||||
/>
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
copy: container.querySelector<HTMLButtonElement>('[data-u-comp="cell-link-popup-copy"]'),
|
||||
edit: container.querySelector('[data-u-comp="cell-link-popup-edit"]') as HTMLElement,
|
||||
remove: container.querySelector('[data-u-comp="cell-link-popup-remove"]') as HTMLElement,
|
||||
};
|
||||
}
|
||||
|
||||
describe('CellLinkPopupPure', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createPopupTestBed> | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('opens the edit panel for the selected sheet hyperlink range', async () => {
|
||||
currentTestBed = createPopupTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
const executedCommands: Array<{ id: string; params: unknown }> = [];
|
||||
currentTestBed.commandService.onCommandExecuted((command) => {
|
||||
executedCommands.push({ id: command.id, params: command.params });
|
||||
});
|
||||
const actions = renderPopup(root, container, currentTestBed);
|
||||
expect(actions.copy?.disabled).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
actions.edit.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(executedCommands).toContainEqual({
|
||||
id: OpenHyperLinkEditPanelOperation.id,
|
||||
params: {
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
row: 0,
|
||||
col: 0,
|
||||
customRangeId: 'link-range',
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('labels the mobile navigation action and shows the hyperlink name', () => {
|
||||
currentTestBed = createPopupTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
currentTestBed.injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
act(() => {
|
||||
root!.render(
|
||||
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
|
||||
<MobileCellLinkPopupPure
|
||||
unitId={UNIT_ID}
|
||||
subUnitId={SUB_UNIT_ID}
|
||||
row={0}
|
||||
col={0}
|
||||
customRange={currentTestBed!.customRange}
|
||||
type={HyperLinkEditSourceType.VIEWING}
|
||||
/>
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const openLinkButton = container.querySelector('button');
|
||||
expect(openLinkButton?.textContent).toContain('Open link');
|
||||
expect(openLinkButton?.textContent).toContain('https://univer.ai');
|
||||
});
|
||||
|
||||
it('removes the hyperlink custom range from the sheet cell', () => {
|
||||
currentTestBed = createPopupTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
const actions = renderPopup(root, container, currentTestBed);
|
||||
|
||||
act(() => {
|
||||
actions.remove.click();
|
||||
});
|
||||
|
||||
const cell = currentTestBed.workbook.getSheetBySheetId(SUB_UNIT_ID)?.getCellRaw(0, 0);
|
||||
|
||||
expect(cell?.p?.body?.customRanges?.some((range) => range.rangeId === 'link-range')).toBe(false);
|
||||
expect(cell?.p?.body?.dataStream).toBe('Univer\r\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CellLinkEdit', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createPopupTestBed> | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('inherits the page direction for labels and uses left-to-right direction for URLs', () => {
|
||||
currentTestBed = createPopupTestBed();
|
||||
container = document.createElement('div');
|
||||
container.dir = 'rtl';
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
const popupService = currentTestBed.injector.get(ISheetsHyperLinkPopupService);
|
||||
|
||||
act(() => {
|
||||
popupService.startAddEditing({
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
row: 1,
|
||||
col: 0,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
});
|
||||
|
||||
for (const LinkEdit of [CellLinkEdit, MobileCellLinkEdit]) {
|
||||
act(() => {
|
||||
root!.render(
|
||||
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
|
||||
<LinkEdit />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const inputs = Array.from(container.querySelectorAll('input'));
|
||||
expect(inputs).toHaveLength(2);
|
||||
expect(inputs[0].hasAttribute('dir')).toBe(false);
|
||||
expect(inputs[1].dir).toBe('ltr');
|
||||
}
|
||||
});
|
||||
|
||||
it('adds a URL hyperlink to the current cell text and closes the popup', async () => {
|
||||
currentTestBed = createPopupTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
const popupService = currentTestBed.injector.get(ISheetsHyperLinkPopupService);
|
||||
const renderManagerService = currentTestBed.injector.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
|
||||
act(() => {
|
||||
popupService.startAddEditing({
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
row: 0,
|
||||
col: 0,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
root!.render(
|
||||
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
|
||||
<CellLinkEdit />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const inputs = Array.from(container.querySelectorAll('input'));
|
||||
expect(inputs).toHaveLength(1);
|
||||
|
||||
await act(async () => {
|
||||
inputText(inputs[0], 'docs.univer.ai');
|
||||
await Promise.resolve();
|
||||
});
|
||||
renderManagerService.enableScrollRender = true;
|
||||
|
||||
const formButtons = Array.from(container.querySelectorAll<HTMLButtonElement>('[data-u-comp="button"]'));
|
||||
const okButton = formButtons[formButtons.length - 1];
|
||||
expect(okButton).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
okButton!.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const cell = currentTestBed.workbook.getSheetBySheetId(SUB_UNIT_ID)?.getCellRaw(0, 0);
|
||||
const body = cell?.p?.body;
|
||||
const linkRange = body?.customRanges?.find((range) => range.rangeType === CustomRangeType.HYPERLINK);
|
||||
|
||||
expect(body?.dataStream).toBe('Univer\r\n');
|
||||
expect(linkRange?.properties?.url).toBe('http://docs.univer.ai');
|
||||
expect(popupService.currentEditing).toBeNull();
|
||||
});
|
||||
|
||||
it('switches from URL to worksheet link and saves the selected sheet target', async () => {
|
||||
currentTestBed = createPopupTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
const popupService = currentTestBed.injector.get(ISheetsHyperLinkPopupService);
|
||||
const renderManagerService = currentTestBed.injector.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
|
||||
act(() => {
|
||||
popupService.startAddEditing({
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
row: 0,
|
||||
col: 0,
|
||||
type: HyperLinkEditSourceType.VIEWING,
|
||||
});
|
||||
root!.render(
|
||||
<RediContext.Provider value={{ injector: currentTestBed!.injector }}>
|
||||
<CellLinkEdit />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const typeSelect = container.querySelector('[data-u-comp="select"]');
|
||||
expect(typeSelect?.textContent).toContain('sheets-hyper-link-ui.form.link');
|
||||
|
||||
await act(async () => {
|
||||
await selectOption(typeSelect!, 'sheets-hyper-link-ui.form.worksheet');
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const selects = Array.from(container.querySelectorAll('[data-u-comp="select"]'));
|
||||
expect(selects).toHaveLength(2);
|
||||
|
||||
await act(async () => {
|
||||
await selectOption(selects[1], 'Sheet 1');
|
||||
await Promise.resolve();
|
||||
});
|
||||
renderManagerService.enableScrollRender = true;
|
||||
|
||||
const formButtons = Array.from(container.querySelectorAll<HTMLButtonElement>('[data-u-comp="button"]'));
|
||||
const okButton = formButtons[formButtons.length - 1];
|
||||
|
||||
await act(async () => {
|
||||
okButton!.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const cell = currentTestBed.workbook.getSheetBySheetId(SUB_UNIT_ID)?.getCellRaw(0, 0);
|
||||
const body = cell?.p?.body;
|
||||
const sheetLink = body?.customRanges?.find((range) => range.properties?.url === `#${SheetHyperLinkType.SHEET}=${SUB_UNIT_ID}`);
|
||||
|
||||
expect(body?.dataStream).toBe('Univer\r\n');
|
||||
expect(sheetLink?.rangeType).toBe(CustomRangeType.HYPERLINK);
|
||||
expect(popupService.currentEditing).toBeNull();
|
||||
});
|
||||
});
|
||||
27
packages/sheets-hyper-link-ui/tailwind.config.ts
Normal file
27
packages/sheets-hyper-link-ui/tailwind.config.ts
Normal 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;
|
||||
8
packages/sheets-hyper-link-ui/tsconfig.json
Normal file
8
packages/sheets-hyper-link-ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/sheets-hyper-link-ui/tsconfig.node.json
Normal file
18
packages/sheets-hyper-link-ui/tsconfig.node.json
Normal 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"
|
||||
]
|
||||
}
|
||||
3
packages/sheets-hyper-link-ui/vitest.config.ts
Normal file
3
packages/sheets-hyper-link-ui/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user