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

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

View File

@@ -0,0 +1,50 @@
# @univerjs/sheets-hyper-link
[![npm version](https://img.shields.io/npm/v/@univerjs/sheets-hyper-link?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-hyper-link)
[![license](https://img.shields.io/npm/l/@univerjs/sheets-hyper-link?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-hyper-link)
[![downloads](https://img.shields.io/npm/dm/@univerjs/sheets-hyper-link?style=flat-square)](https://npmjs.com/package/@univerjs/sheets-hyper-link)
`@univerjs/sheets-hyper-link` adds the core hyperlink model, commands, and sheet-specific services for Univer Sheets.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/sheets-hyper-link` | `UniverSheetsHyperLink` | No | Yes | Yes |
## Installation
```sh
pnpm add @univerjs/sheets-hyper-link
# or
npm install @univerjs/sheets-hyper-link
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import { LocaleType, mergeLocales, Univer } from '@univerjs/core';
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
import EnUS from '@univerjs/sheets-hyper-link/locale/en-US';
const univer = new Univer({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(EnUS),
},
});
univer.registerPlugin(UniverSheetsHyperLinkPlugin);
```
## Integration Notes
Use this package with `@univerjs/sheets-hyper-link-ui` when users need hyperlink editing UI.
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/sheets-hyper-link)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,98 @@
{
"name": "@univerjs/sheets-hyper-link",
"version": "1.0.0-rc.0",
"private": false,
"description": "Hyperlink model and commands 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",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./facade": "./src/facade/index.ts",
"./locale/*": "./src/locale/*.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": {
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/docs": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/sheets": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"rxjs": "^7.8.2",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1,397 @@
/**
* 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, ICommand, ICustomRange, IDocumentData, IWorkbookData, Workbook } from '@univerjs/core';
import {
CellValueType,
createIdentifier,
CustomRangeType,
ICommandService,
IUniverInstanceService,
LocaleType,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { DocSelectionManagerService, DocStateEmitService, RichTextEditingMutation } from '@univerjs/docs';
import { SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { HyperLinkModel } from '../../../models/hyper-link.model';
import { AddHyperLinkMutation } from '../../mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../../mutations/remove-hyper-link.mutation';
import { UpdateHyperLinkMutation } from '../../mutations/update-hyper-link.mutation';
import { AddHyperLinkCommand, AddRichHyperLinkCommand } from '../add-hyper-link.command';
import { CancelHyperLinkCommand, CancelRichHyperLinkCommand } from '../remove-hyper-link.command';
import { UpdateHyperLinkCommand, UpdateRichHyperLinkCommand } from '../update-hyper-link.command';
const unitId = 'hyper-link-command-workbook';
const subUnitId = 'sheet-1';
const documentId = 'rich-hyper-link-doc';
const IRenderManagerServiceIdentifier = createIdentifier<TestRenderManagerService>('engine-render.render-manager.service');
class TestRenderManagerService {
getRenderUnitById() {
return null;
}
}
function createCellDocument(text: string, rangeId: string, url: string) {
return {
id: `${rangeId}-doc`,
documentStyle: {
pageSize: { width: 100, height: 40 },
marginTop: 0,
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
},
body: {
dataStream: `${text}\r\n`,
customRanges: [{
rangeId,
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: text.length - 1,
properties: {
url,
},
}],
},
};
}
function createWorkbookData(): IWorkbookData {
return {
id: unitId,
appVersion: '3.0.0-alpha',
name: 'hyperlink command workbook',
locale: LocaleType.EN_US,
sheetOrder: [subUnitId],
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet 1',
cellData: {
1: {
1: {
p: createCellDocument('Old', 'cancel-link', 'https://old.invalid'),
t: CellValueType.STRING,
},
},
2: {
2: {
p: createCellDocument('Before', 'update-link', 'https://before.invalid'),
t: CellValueType.STRING,
},
},
},
},
},
};
}
function createDocData(): IDocumentData {
return {
id: documentId,
body: {
dataStream: 'Existing\r\n',
customRanges: [{
rangeId: 'rich-existing',
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 7,
properties: {
url: 'https://before.invalid',
},
}],
},
documentStyle: {
pageSize: {
width: 594.3,
height: 840.51,
},
marginTop: 72,
marginBottom: 72,
marginRight: 90,
marginLeft: 90,
},
};
}
function findHyperLinkRange(workbook: Workbook, row: number, column: number, rangeId: string): ICustomRange | undefined {
const ranges = workbook.getSheetBySheetId(subUnitId)?.getCellRaw(row, column)?.p?.body?.customRanges ?? [];
for (const range of ranges) {
if (range.rangeId === rangeId) {
return range;
}
}
return undefined;
}
describe('sheet hyperlink commands', () => {
let univer: Univer;
let commandService: ICommandService;
let workbook: Workbook;
let doc: DocumentDataModel;
let hyperLinkModel: HyperLinkModel;
beforeEach(() => {
univer = new Univer();
const injector = univer.__getInjector();
injector.add([SheetInterceptorService]);
injector.add([HyperLinkModel]);
injector.add([DocSelectionManagerService]);
injector.add([DocStateEmitService]);
injector.add([IRenderManagerServiceIdentifier, { useClass: TestRenderManagerService }]);
workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
doc = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, createDocData());
injector.get(IUniverInstanceService).focusUnit(unitId);
commandService = injector.get(ICommandService);
commandService.registerCommand(SetRangeValuesMutation);
commandService.registerCommand(AddHyperLinkMutation);
commandService.registerCommand(UpdateHyperLinkMutation);
commandService.registerCommand(RemoveHyperLinkMutation);
commandService.registerCommand(AddHyperLinkCommand);
commandService.registerCommand(UpdateHyperLinkCommand);
commandService.registerCommand(CancelHyperLinkCommand);
commandService.registerCommand(AddRichHyperLinkCommand);
commandService.registerCommand(UpdateRichHyperLinkCommand);
commandService.registerCommand(CancelRichHyperLinkCommand);
commandService.registerCommand(RichTextEditingMutation as unknown as ICommand);
hyperLinkModel = injector.get(HyperLinkModel);
});
afterEach(() => {
univer.dispose();
});
it('adds a hyperlink display value to the selected sheet cell', async () => {
const result = await commandService.executeCommand(AddHyperLinkCommand.id, {
unitId,
subUnitId,
link: {
id: 'added-link',
row: 0,
column: 0,
payload: 'https://added.invalid',
display: 'Added',
},
});
const cell = workbook.getSheetBySheetId(subUnitId)?.getCellRaw(0, 0);
const range = findHyperLinkRange(workbook, 0, 0, 'added-link');
expect(result).toBe(true);
expect(cell?.p?.body?.dataStream).toBe('Added\r\n');
expect(range).toMatchObject({
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 4,
properties: {
url: 'https://added.invalid',
},
});
});
it('replaces the previous hyperlink model when adding a new link to the same cell', async () => {
hyperLinkModel.addHyperLink(unitId, subUnitId, {
id: 'cancel-link',
row: 1,
column: 1,
payload: 'https://old.invalid',
display: 'Old',
});
const result = await commandService.executeCommand(AddHyperLinkCommand.id, {
unitId,
subUnitId,
link: {
id: 'replacement-link',
row: 1,
column: 1,
payload: 'https://replacement.invalid',
display: 'Replacement',
},
});
const cell = workbook.getSheetBySheetId(subUnitId)?.getCellRaw(1, 1);
const range = findHyperLinkRange(workbook, 1, 1, 'replacement-link');
expect(result).toBe(true);
expect(cell?.p?.body?.dataStream).toBe('Replacement\r\n');
expect(range).toMatchObject({
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 10,
properties: {
url: 'https://replacement.invalid',
},
});
expect(hyperLinkModel.getHyperLink(unitId, subUnitId, 'cancel-link')).toBeUndefined();
});
it('removes a hyperlink from the cell document and hyperlink model', async () => {
hyperLinkModel.addHyperLink(unitId, subUnitId, {
id: 'cancel-link',
row: 1,
column: 1,
payload: 'https://old.invalid',
display: 'Old',
});
const result = await commandService.executeCommand(CancelHyperLinkCommand.id, {
unitId,
subUnitId,
id: 'cancel-link',
row: 1,
column: 1,
});
expect(result).toBe(true);
expect(findHyperLinkRange(workbook, 1, 1, 'cancel-link')).toBeUndefined();
expect(hyperLinkModel.getHyperLink(unitId, subUnitId, 'cancel-link')).toBeUndefined();
});
it('updates a hyperlink label and target in the cell document', async () => {
hyperLinkModel.addHyperLink(unitId, subUnitId, {
id: 'update-link',
row: 2,
column: 2,
payload: 'https://before.invalid',
display: 'Before',
});
const result = await commandService.executeCommand(UpdateHyperLinkCommand.id, {
unitId,
subUnitId,
id: 'update-link',
row: 2,
column: 2,
payload: {
payload: 'https://after.invalid',
display: 'After',
},
});
const cell = workbook.getSheetBySheetId(subUnitId)?.getCellRaw(2, 2);
const updatedRange = cell?.p?.body?.customRanges?.[0];
expect(result).toBe(true);
expect(cell?.p?.body?.dataStream).toBe('After\r\n');
expect(updatedRange).toMatchObject({
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 4,
properties: {
url: 'https://after.invalid',
},
});
expect(hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, 2, 2)).toBeUndefined();
});
it('adds a rich hyperlink to the selected document text', async () => {
const selectionManager = univer.__getInjector().get(DocSelectionManagerService);
selectionManager.__TEST_ONLY_setCurrentSelection({
unitId: documentId,
subUnitId: documentId,
});
selectionManager.__TEST_ONLY_add([{
startOffset: 0,
endOffset: 8,
collapsed: false,
isActive: true,
segmentId: '',
style: null as never,
}]);
const result = await commandService.executeCommand(AddRichHyperLinkCommand.id, {
documentId,
link: {
id: 'ignored-source-id',
row: 0,
column: 0,
payload: 'https://rich-added.invalid',
},
});
let addedRange;
for (const range of doc.getBody()?.customRanges ?? []) {
if (range.properties?.url === 'https://rich-added.invalid') {
addedRange = range;
break;
}
}
expect(result).toBeTruthy();
expect(addedRange).toMatchObject({
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 7,
properties: {
url: 'https://rich-added.invalid',
},
});
});
it('updates a rich hyperlink label and URL in the document body', async () => {
const result = await commandService.executeCommand(UpdateRichHyperLinkCommand.id, {
documentId,
id: 'rich-existing',
payload: {
payload: 'https://rich-after.invalid',
display: 'Updated',
},
});
const range = doc.getBody()?.customRanges?.[0];
expect(result).toBeTruthy();
expect(doc.getBody()?.dataStream).toBe('Updated\r\n');
expect(range).toMatchObject({
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 6,
properties: {
url: 'https://rich-after.invalid',
},
});
});
it('removes a rich hyperlink from the document body', async () => {
const result = await commandService.executeCommand(CancelRichHyperLinkCommand.id, {
documentId,
unitId,
subUnitId,
id: 'rich-existing',
row: 0,
column: 0,
});
let existingRange;
for (const range of doc.getBody()?.customRanges ?? []) {
if (range.rangeId === 'rich-existing') {
existingRange = range;
break;
}
}
expect(result).toBeTruthy();
expect(existingRange).toBeUndefined();
});
});

View File

@@ -0,0 +1,209 @@
/**
* 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, ICommand, IDocumentData, IMutationInfo } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
import type { ISheetHyperLink } from '../../types/interfaces/i-hyper-link';
import { BuildTextUtils, CellValueType, CommandType, CustomRangeType, generateRandomId, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute, TextX, Tools } from '@univerjs/core';
import { addCustomRangeBySelectionFactory } from '@univerjs/docs';
import { getSheetCommandTarget, SetRangeValuesMutation, SetRangeValuesUndoMutationFactory, SheetInterceptorService } from '@univerjs/sheets';
import { HyperLinkModel } from '../../models/hyper-link.model';
import { AddHyperLinkMutation } from '../mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../mutations/remove-hyper-link.mutation';
export interface IAddHyperLinkCommandParams {
unitId: string;
subUnitId: string;
link: ISheetHyperLink;
}
/**
* Command for add hyperlink
*/
export const AddHyperLinkCommand: ICommand<IAddHyperLinkCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.add-hyper-link',
// eslint-disable-next-line max-lines-per-function
async handler(accessor, params) {
if (!params) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const hyperLinkModel = accessor.get(HyperLinkModel);
const sheetInterceptorService = accessor.get(SheetInterceptorService);
const target = getSheetCommandTarget(univerInstanceService, params);
if (!target) return false;
const { unitId, subUnitId, workbook, worksheet } = target;
const { link } = params;
const { payload, display, row, column, id } = link;
const cellData = worksheet.getCell(row, column);
const doc = worksheet.getBlankCellDocumentModel(cellData, row, column);
const snapshot = doc.documentModel!.getSnapshot();
const body = Tools.deepClone(snapshot.body);
if (!body) return false;
let textX: TextX | false;
if (display) {
textX = BuildTextUtils.selection.replace({
selection: {
startOffset: 0,
endOffset: body.dataStream.length - 2,
collapsed: body.dataStream.length - 2 === 0,
},
body: {
dataStream: `${display}`,
customRanges: [{
startIndex: 0,
endIndex: display.length - 1,
rangeType: CustomRangeType.HYPERLINK,
rangeId: id,
properties: {
url: payload,
// refId: id,
},
}],
},
doc: doc.documentModel!,
});
} else {
textX = BuildTextUtils.customRange.add({
body,
ranges: [{ startOffset: 0, endOffset: body.dataStream.length - 2, collapsed: false }],
rangeId: id,
rangeType: CustomRangeType.HYPERLINK,
properties: {
url: payload,
refId: id,
},
});
}
if (!textX) return false;
const newBody = TextX.apply(body, textX.serialize());
const rangeValue: IDocumentData = {
...snapshot,
body: newBody,
};
const newCellData: ICellData = {
p: rangeValue,
t: CellValueType.STRING,
};
const finalCellData = sheetInterceptorService.onWriteCell(workbook, worksheet, row, column, newCellData);
const redoParams: ISetRangeValuesMutationParams = {
unitId,
subUnitId,
cellValue: {
[link.row]: {
[link.column]: finalCellData,
},
},
};
const redo: IMutationInfo = {
id: SetRangeValuesMutation.id,
params: redoParams,
};
const undoParams = SetRangeValuesUndoMutationFactory(accessor, redoParams);
const undo: IMutationInfo = {
id: SetRangeValuesMutation.id,
params: undoParams,
};
const redos = [redo];
const undos = [undo];
const modelLink = hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, column);
if (modelLink) {
redos.push({
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: modelLink.id,
},
});
undos.push({
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link: modelLink,
},
});
}
const res = await sequenceExecute(redos, commandService);
if (res) {
const isValid = await sheetInterceptorService.onValidateCell(workbook, worksheet, row, column);
if (isValid === false) {
sequenceExecute(undos, commandService);
return false;
}
undoRedoService.pushUndoRedo({
redoMutations: redos,
undoMutations: undos,
unitID: unitId,
});
return true;
}
return false;
},
};
export interface IAddRichHyperLinkCommandParams {
/**
* url of link
*/
documentId: string;
link: ISheetHyperLink;
}
export const AddRichHyperLinkCommand: ICommand<IAddRichHyperLinkCommandParams> = {
id: 'sheets.command.add-rich-hyper-link',
type: CommandType.COMMAND,
handler: async (accessor, params) => {
if (!params) {
return false;
}
const { documentId, link } = params;
const commandService = accessor.get(ICommandService);
const newId = generateRandomId();
const { payload } = link;
const replaceSelection = addCustomRangeBySelectionFactory(accessor, {
unitId: documentId,
rangeId: newId,
rangeType: CustomRangeType.HYPERLINK,
properties: {
url: payload,
refId: newId,
},
});
if (replaceSelection) {
return commandService.syncExecuteCommand(replaceSelection.id, replaceSelection.params);
}
return false;
},
};

View File

@@ -0,0 +1,168 @@
/**
* 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, ICommand, IDocumentBody, IMutationInfo, Nullable } from '@univerjs/core';
import type { IAddHyperLinkMutationParams } from '../mutations/add-hyper-link.mutation';
import { BuildTextUtils, CellValueType, CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute, TextX, Tools, UniverInstanceType } from '@univerjs/core';
import { deleteCustomRangeFactory } from '@univerjs/docs';
import { getSheetCommandTarget, SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '@univerjs/sheets';
import { HyperLinkModel } from '../../models/hyper-link.model';
import { AddHyperLinkMutation } from '../mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../mutations/remove-hyper-link.mutation';
export interface ICancelHyperLinkCommandParams {
unitId: string;
subUnitId: string;
/**
* id of link
*/
id: string;
row: number;
column: number;
}
export const CancelHyperLinkCommand: ICommand<ICancelHyperLinkCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.cancel-hyper-link',
// eslint-disable-next-line max-lines-per-function
handler(accessor, params) {
if (!params) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const instanceSrv = accessor.get(IUniverInstanceService);
const hyperLinkModel = accessor.get(HyperLinkModel);
const target = getSheetCommandTarget(instanceSrv, params);
if (!target) return false;
const { row, column, id } = params;
const { unitId, subUnitId, worksheet } = target;
const cellData = worksheet.getCell(row, column);
if (!cellData) return false;
const doc = worksheet.getCellDocumentModelWithFormula(cellData, row, column);
if (!doc?.documentModel) return false;
const snapshot = Tools.deepClone(doc.documentModel!.getSnapshot());
const range = snapshot.body?.customRanges?.find((range) => `${range.rangeId}` === id);
if (!range) return false;
const textX = BuildTextUtils.customRange.delete({ documentDataModel: doc.documentModel, rangeId: range.rangeId });
if (!textX) return false;
const newBody = TextX.apply(snapshot.body!, textX.serialize());
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const setRangeParams = {
unitId,
subUnitId,
cellValue: {
[row]: {
[column]: {
p: {
...snapshot,
body: newBody,
},
t: CellValueType.STRING,
},
},
},
};
redos.push({
id: SetRangeValuesMutation.id,
params: setRangeParams,
});
const undoParams = SetRangeValuesUndoMutationFactory(accessor, setRangeParams);
undos.push({
id: SetRangeValuesMutation.id,
params: undoParams,
});
const link = hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, column);
if (link) {
redos.push({
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
},
});
undos.push({
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link: {
...link,
},
} as IAddHyperLinkMutationParams,
});
}
const res = sequenceExecute(redos, commandService).result;
if (res) {
undoRedoService.pushUndoRedo({
redoMutations: redos,
undoMutations: undos,
unitID: unitId,
});
return true;
}
return false;
},
};
export interface ICancelRichHyperLinkCommandParams extends ICancelHyperLinkCommandParams {
/**
* document id
*/
documentId: string;
}
export const CancelRichHyperLinkCommand: ICommand<ICancelRichHyperLinkCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.cancel-rich-hyper-link',
handler(accessor, params) {
if (!params) {
return false;
}
const { id: linkId, documentId } = params;
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const doc = univerInstanceService.getUnit<DocumentDataModel>(documentId, UniverInstanceType.UNIVER_DOC);
const link = doc?.getBody()?.customRanges?.find((i) => i.rangeId === linkId);
let insert: Nullable<IDocumentBody> = null;
if (link && link.endIndex === doc!.getBody()!.dataStream.length - 3) {
insert = {
dataStream: ' ',
};
}
const doMutation = deleteCustomRangeFactory(accessor, { unitId: documentId, rangeId: linkId, insert });
if (!doMutation) {
return false;
}
return commandService.syncExecuteCommand(doMutation.id, doMutation.params);
},
};

View File

@@ -0,0 +1,232 @@
/**
* 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, ICellData, ICommand, IMutationInfo } from '@univerjs/core';
import type { ICellLinkContent } from '../../types/interfaces/i-hyper-link';
import { CellValueType, CommandType, CustomRangeType, generateRandomId, getBodySlice, ICommandService, IUndoRedoService, IUniverInstanceService, sequenceExecute, TextX, Tools, UniverInstanceType } from '@univerjs/core';
import { replaceSelectionFactory } from '@univerjs/docs';
import { getSheetCommandTarget, SetRangeValuesMutation, SetRangeValuesUndoMutationFactory, SheetInterceptorService } from '@univerjs/sheets';
import { HyperLinkModel } from '../../models/hyper-link.model';
import { AddHyperLinkMutation } from '../mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../mutations/remove-hyper-link.mutation';
export interface IUpdateHyperLinkCommandParams {
unitId: string;
subUnitId: string;
id: string;
payload: ICellLinkContent;
row: number;
column: number;
}
export const UpdateHyperLinkCommand: ICommand<IUpdateHyperLinkCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.update-hyper-link',
// eslint-disable-next-line max-lines-per-function
async handler(accessor, params) {
if (!params) return false;
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const instanceSrv = accessor.get(IUniverInstanceService);
const hyperLinkModel = accessor.get(HyperLinkModel);
const interceptorService = accessor.get(SheetInterceptorService);
const target = getSheetCommandTarget(instanceSrv, {
unitId: params.unitId,
subUnitId: params.subUnitId,
});
if (!target) return false;
const { payload: link, row, column, id } = params;
const { workbook, worksheet, unitId, subUnitId } = target;
const { payload, display = '' } = link;
const cellData = worksheet.getCell(row, column);
if (!cellData) return false;
const doc = worksheet.getCellDocumentModelWithFormula(cellData, row, column);
if (!doc?.documentModel) return false;
const snapshot = doc.documentModel.getSnapshot();
const range = snapshot.body?.customRanges?.find((range) => `${range.rangeId}` === id);
if (!range) return false;
const newId = generateRandomId();
const oldBody = getBodySlice(doc.documentModel.getBody()!, range.startIndex, range.endIndex + 1);
const textRun = oldBody.textRuns?.[0];
if (textRun) {
textRun.ed = display.length + 1;
}
const replaceSelection = replaceSelectionFactory(accessor, {
unitId,
body: {
dataStream: `${display}`,
customRanges: [{
rangeId: newId,
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: display.length - 1,
properties: {
url: payload,
},
}],
textRuns: textRun ? [textRun] : undefined,
},
selection: {
startOffset: range.startIndex,
endOffset: range.endIndex + 1,
collapsed: false,
},
doc: doc.documentModel,
});
if (!replaceSelection) {
return false;
}
const newBody = TextX.apply(Tools.deepClone(snapshot.body!), replaceSelection.textX.serialize());
const newCellData: ICellData = {
p: {
...snapshot,
body: newBody,
},
t: CellValueType.STRING,
};
const finalCellData = interceptorService.onWriteCell(workbook, worksheet, row, column, newCellData);
const redo = {
id: SetRangeValuesMutation.id,
params: {
unitId,
subUnitId,
cellValue: {
[row]: {
[column]: finalCellData,
},
},
},
};
const undoParams = SetRangeValuesUndoMutationFactory(accessor, redo.params);
const undo = {
id: SetRangeValuesMutation.id,
params: undoParams,
};
const redos: IMutationInfo[] = [redo];
const undos: IMutationInfo[] = [undo];
const modelLink = hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, column);
if (modelLink) {
redos.push({
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: modelLink.id,
},
});
undos.push({
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link: modelLink,
},
});
}
const res = sequenceExecute(redos, commandService);
if (res.result) {
const isValid = await interceptorService.onValidateCell(workbook, worksheet, row, column);
if (isValid === false) {
sequenceExecute(undos, commandService);
return false;
}
undoRedoService.pushUndoRedo({
redoMutations: redos,
undoMutations: undos,
unitID: unitId,
});
return true;
}
return false;
},
};
export interface IUpdateRichHyperLinkCommandParams {
documentId: string;
id: string;
payload: ICellLinkContent;
}
export const UpdateRichHyperLinkCommand: ICommand<IUpdateRichHyperLinkCommandParams> = {
type: CommandType.COMMAND,
id: 'sheets.command.update-rich-hyper-link',
handler: (accessor, params) => {
if (!params) {
return false;
}
const { documentId: unitId, payload, id: rangeId } = params;
const univerInstanceService = accessor.get(IUniverInstanceService);
const commandService = accessor.get(ICommandService);
const doc = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
if (!doc) {
return false;
}
const range = doc.getBody()?.customRanges?.find((range) => range.rangeId === rangeId);
if (!range) {
return false;
}
const display = params.payload.display ?? '';
const newId = generateRandomId();
const oldBody = getBodySlice(doc.getBody()!, range.startIndex, range.endIndex + 1);
const textRun = oldBody.textRuns?.[0];
if (textRun) {
textRun.ed = display.length + 1;
}
const replaceSelection = replaceSelectionFactory(accessor, {
unitId,
body: {
dataStream: `${display}`,
customRanges: [{
rangeId: newId,
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: display.length - 1,
properties: {
url: payload.payload,
},
}],
textRuns: textRun ? [textRun] : undefined,
},
selection: {
startOffset: range.startIndex,
endOffset: range.endIndex + 1,
collapsed: false,
},
doc,
});
if (!replaceSelection) {
return false;
}
return commandService.syncExecuteCommand(replaceSelection.id, replaceSelection.params);
},
};

View File

@@ -0,0 +1,200 @@
/**
* 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 { IWorkbookData, Workbook } from '@univerjs/core';
import type { ISheetHyperLink } from '../../../types/interfaces/i-hyper-link';
import { CustomRangeType, ICommandService, LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { HyperLinkModel } from '../../../models/hyper-link.model';
import { AddHyperLinkMutation } from '../add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../remove-hyper-link.mutation';
import { UpdateHyperLinkMutation, UpdateHyperLinkRefMutation, UpdateRichHyperLinkMutation } from '../update-hyper-link.mutation';
const unitId = 'hyper-link-workbook';
const subUnitId = 'sheet-1';
const workbookData: IWorkbookData = {
id: unitId,
appVersion: '3.0.0-alpha',
name: 'hyperlink workbook',
locale: LocaleType.EN_US,
sheetOrder: [subUnitId],
styles: {},
sheets: {
[subUnitId]: {
id: subUnitId,
name: 'Sheet 1',
cellData: {
4: {
3: {
p: {
id: 'cell-doc',
documentStyle: {
pageSize: {
width: 100,
height: 40,
},
marginTop: 0,
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
},
body: {
dataStream: 'Docs\r\n',
customRanges: [{
rangeId: 'rich-link',
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 3,
properties: {
url: 'https://old.invalid',
},
}],
},
},
},
},
},
},
},
};
function createLink(overrides: Partial<ISheetHyperLink> = {}): ISheetHyperLink {
return {
id: 'link-1',
row: 0,
column: 0,
payload: 'https://univer.ai',
display: 'Univer',
...overrides,
};
}
describe('sheet hyperlink mutations', () => {
let univer: Univer;
let commandService: ICommandService;
let hyperLinkModel: HyperLinkModel;
let workbook: Workbook;
beforeEach(() => {
univer = new Univer();
const injector = univer.__getInjector();
injector.add([HyperLinkModel]);
commandService = injector.get(ICommandService);
commandService.registerCommand(AddHyperLinkMutation);
commandService.registerCommand(UpdateHyperLinkMutation);
commandService.registerCommand(UpdateHyperLinkRefMutation);
commandService.registerCommand(UpdateRichHyperLinkMutation);
commandService.registerCommand(RemoveHyperLinkMutation);
hyperLinkModel = injector.get(HyperLinkModel);
workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData);
});
afterEach(() => {
univer.dispose();
});
it('adds and updates the hyperlink attached to a cell', async () => {
const addResult = await commandService.executeCommand(AddHyperLinkMutation.id, {
unitId,
subUnitId,
link: createLink(),
});
const updateResult = await commandService.executeCommand(UpdateHyperLinkMutation.id, {
unitId,
subUnitId,
id: 'link-1',
payload: {
payload: 'https://docs.univer.ai',
display: 'Docs',
},
});
expect(addResult).toBe(true);
expect(updateResult).toBe(true);
expect(hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, 0, 0)).toMatchObject({
id: 'link-1',
payload: 'https://docs.univer.ai',
display: 'Docs',
});
});
it('moves a hyperlink to a new cell and clears its original location', async () => {
await commandService.executeCommand(AddHyperLinkMutation.id, {
unitId,
subUnitId,
link: createLink(),
});
const moveResult = await commandService.executeCommand(UpdateHyperLinkRefMutation.id, {
unitId,
subUnitId,
id: 'link-1',
row: 3,
column: 2,
});
expect(moveResult).toBe(true);
expect(hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, 0, 0)).toBeUndefined();
expect(hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, 3, 2)).toMatchObject({
id: 'link-1',
row: 3,
column: 2,
});
});
it('removes a hyperlink from both id and cell lookup', async () => {
await commandService.executeCommand(AddHyperLinkMutation.id, {
unitId,
subUnitId,
link: createLink(),
});
const removeResult = await commandService.executeCommand(RemoveHyperLinkMutation.id, {
unitId,
subUnitId,
id: 'link-1',
});
expect(removeResult).toBe(true);
expect(hyperLinkModel.getHyperLink(unitId, subUnitId, 'link-1')).toBeUndefined();
expect(hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, 0, 0)).toBeUndefined();
});
it('updates the URL stored in a rich hyperlink cell range', async () => {
const result = await commandService.executeCommand(UpdateRichHyperLinkMutation.id, {
unitId,
subUnitId,
row: 4,
col: 3,
id: 'rich-link',
url: 'https://new.invalid',
});
let updatedUrl = '';
const ranges = workbook.getSheetBySheetId(subUnitId)?.getCellRaw(4, 3)?.p?.body?.customRanges ?? [];
for (const range of ranges) {
if (range.rangeId === 'rich-link') {
updatedUrl = range.properties?.url ?? '';
break;
}
}
expect(result).toBe(true);
expect(updatedUrl).toBe('https://new.invalid');
});
});

View 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 { ICommand } from '@univerjs/core';
import type { ISheetHyperLink } from '../../types/interfaces/i-hyper-link';
import { CommandType } from '@univerjs/core';
import { HyperLinkModel } from '../../models/hyper-link.model';
export interface IAddHyperLinkMutationParams {
unitId: string;
subUnitId: string;
link: ISheetHyperLink;
}
export const AddHyperLinkMutation: ICommand<IAddHyperLinkMutationParams> = {
type: CommandType.MUTATION,
id: 'sheets.mutation.add-hyper-link',
handler(accessor, params) {
if (!params) {
return false;
}
const model = accessor.get(HyperLinkModel);
const { unitId, subUnitId, link } = params;
return model.addHyperLink(unitId, subUnitId, link);
},
};

View 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 { ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import { HyperLinkModel } from '../../models/hyper-link.model';
export interface IRemoveHyperLinkMutationParams {
unitId: string;
subUnitId: string;
id: string;
}
export const RemoveHyperLinkMutation: ICommand<IRemoveHyperLinkMutationParams> = {
type: CommandType.MUTATION,
id: 'sheets.mutation.remove-hyper-link',
handler(accessor, params) {
if (!params) {
return false;
}
const model = accessor.get(HyperLinkModel);
const { unitId, subUnitId, id } = params;
return model.removeHyperLink(unitId, subUnitId, id);
},
};

View File

@@ -0,0 +1,101 @@
/**
* 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 { ICellLinkContent } from '../../types/interfaces/i-hyper-link';
import { CommandType, CustomRangeType, IUniverInstanceService } from '@univerjs/core';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { HyperLinkModel } from '../../models/hyper-link.model';
export interface IUpdateHyperLinkMutationParams {
unitId: string;
subUnitId: string;
id: string;
payload: ICellLinkContent;
}
export const UpdateHyperLinkMutation: ICommand<IUpdateHyperLinkMutationParams> = {
type: CommandType.MUTATION,
id: 'sheets.mutation.update-hyper-link',
handler(accessor, params) {
if (!params) {
return false;
}
const model = accessor.get(HyperLinkModel);
const { unitId, subUnitId, payload, id } = params;
return model.updateHyperLink(unitId, subUnitId, id, payload, false);
},
};
export interface IUpdateHyperLinkRefMutationParams {
unitId: string;
subUnitId: string;
id: string;
row: number;
column: number;
silent?: boolean;
}
export const UpdateHyperLinkRefMutation: ICommand<IUpdateHyperLinkRefMutationParams> = {
type: CommandType.MUTATION,
id: 'sheets.mutation.update-hyper-link-ref',
handler(accessor, params) {
if (!params) {
return false;
}
const model = accessor.get(HyperLinkModel);
const { unitId, subUnitId, id, row, column, silent } = params;
return model.updateHyperLinkRef(unitId, subUnitId, id, { row, column }, silent);
},
};
export interface IUpdateRichHyperLinkMutationParams {
unitId: string;
subUnitId: string;
row: number;
col: number;
id: string;
url: string;
}
export const UpdateRichHyperLinkMutation: ICommand<IUpdateRichHyperLinkMutationParams> = {
type: CommandType.MUTATION,
id: 'sheets.mutation.update-rich-hyper-link',
handler(accessor, params) {
if (!params) {
return false;
}
const { unitId, subUnitId, row, col, id, url } = params;
const univerInstanceService = accessor.get(IUniverInstanceService);
const sheetTarget = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
if (!sheetTarget) {
return false;
}
const { worksheet } = sheetTarget;
const cell = worksheet.getCellRaw(row, col);
const link = cell?.p?.body?.customRanges?.find((range) => range.rangeType === CustomRangeType.HYPERLINK && range.rangeId === id);
if (!link) {
return true;
}
link.properties!.url = url;
return true;
},
};

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SHEETS_HYPER_LINK_PLUGIN_CONFIG_KEY = 'sheets-hyper-link.config';
export const configSymbol = Symbol(SHEETS_HYPER_LINK_PLUGIN_CONFIG_KEY);
export interface IUniverSheetsHyperLinkConfig {
}
export const defaultPluginConfig: IUniverSheetsHyperLinkConfig = {};

View File

@@ -0,0 +1,308 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable, IRange, Nullable } from '@univerjs/core';
import type { EffectRefRangeParams } from '@univerjs/sheets';
import type { IAddHyperLinkMutationParams } from '../commands/mutations/add-hyper-link.mutation';
import type { IRemoveHyperLinkMutationParams } from '../commands/mutations/remove-hyper-link.mutation';
import type { IUpdateHyperLinkMutationParams, IUpdateHyperLinkRefMutationParams } from '../commands/mutations/update-hyper-link.mutation';
import type { ISheetHyperLink } from '../types/interfaces/i-hyper-link';
import { Disposable, ICommandService, Inject, isValidRange, sequenceExecuteAsync, toDisposable } from '@univerjs/core';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import { handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests, handleDefaultRangeChangeWithEffectRefCommandsSkipNoInterests, RefRangeService, SheetsSelectionsService } from '@univerjs/sheets';
import { AddHyperLinkMutation } from '../commands/mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../commands/mutations/remove-hyper-link.mutation';
import { UpdateHyperLinkMutation, UpdateHyperLinkRefMutation } from '../commands/mutations/update-hyper-link.mutation';
import { HyperLinkModel } from '../models/hyper-link.model';
import { ERROR_RANGE } from '../types/const';
export class SheetsHyperLinkRefRangeController extends Disposable {
private _disposableMap = new Map<string, IDisposable>();
private _watchDisposableMap = new Map<string, IDisposable>();
private _rangeDisableMap = new Map<string, IDisposable>();
private _rangeWatcherMap = new Map<string, IDisposable>();
constructor(
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
@Inject(HyperLinkModel) private readonly _hyperLinkModel: HyperLinkModel,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initData();
this._initRefRange();
}
private _handlePositionChange = (unitId: string, subUnitId: string, link: ISheetHyperLink, resultRange: Nullable<IRange>, silent: boolean) => {
const oldRange: IRange = {
startColumn: link.column,
endColumn: link.column,
startRow: link.row,
endRow: link.row,
};
if (!resultRange) {
return {
redos: [{
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
} as IRemoveHyperLinkMutationParams,
}],
undos: [{
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link,
} as IAddHyperLinkMutationParams,
}],
};
}
return {
redos: [{
id: UpdateHyperLinkRefMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
row: resultRange.startRow,
column: resultRange.startColumn,
silent,
} as IUpdateHyperLinkRefMutationParams,
}],
undos: [{
id: UpdateHyperLinkRefMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
row: oldRange.startRow,
column: oldRange.startColumn,
silent,
} as IUpdateHyperLinkRefMutationParams,
}],
};
};
private _registerPosition(unitId: string, subUnitId: string, link: ISheetHyperLink) {
const id = link.id;
const oldRange: IRange = {
startColumn: link.column,
endColumn: link.column,
startRow: link.row,
endRow: link.row,
};
const handleRefRangeChange = (commandInfo: EffectRefRangeParams) => {
const resultRanges = handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests(oldRange, commandInfo, { selectionManagerService: this._selectionManagerService });
const resultRange = Array.isArray(resultRanges) ? resultRanges[0] : resultRanges;
if (resultRange && resultRange.startColumn === oldRange.startColumn && resultRange.startRow === oldRange.startRow) {
return {
undos: [],
redos: [],
};
}
const res = this._handlePositionChange(unitId, subUnitId, link, resultRange, false);
return res;
};
this._disposableMap.set(id, this._refRangeService.registerRefRange(oldRange, handleRefRangeChange, unitId, subUnitId));
}
private _watchPosition(unitId: string, subUnitId: string, link: ISheetHyperLink) {
const id = link.id;
const oldRange: IRange = {
startColumn: link.column,
endColumn: link.column,
startRow: link.row,
endRow: link.row,
};
this._watchDisposableMap.set(id, this._refRangeService.watchRange(unitId, subUnitId, oldRange, (before, after) => {
const { redos } = this._handlePositionChange(unitId, subUnitId, link, after, true);
sequenceExecuteAsync(redos, this._commandService, { onlyLocal: true });
}, true));
}
private _unregisterPosition(id: string) {
const disposable = this._disposableMap.get(id);
disposable?.dispose();
this._disposableMap.delete(id);
}
private _unwatchPosition(id: string) {
const disposable = this._watchDisposableMap.get(id);
disposable?.dispose();
this._watchDisposableMap.delete(id);
}
private _registerRange(unitId: string, id: string, payload: string, silent = false) {
if (payload.startsWith('#')) {
const search = new URLSearchParams(payload.slice(1));
// range, gid, rangeid
const searchObj = {
gid: search.get('gid') ?? '',
range: search.get('range') ?? '',
rangeid: search.get('rangeid') ?? '',
};
if (searchObj.range && searchObj.gid) {
const subUnitId = searchObj.gid;
const range = deserializeRangeWithSheet(searchObj.range).range;
if (isValidRange(range) && searchObj.range !== ERROR_RANGE) {
const handleRangeChange = (commandInfo: EffectRefRangeParams) => {
const resultRange = handleDefaultRangeChangeWithEffectRefCommandsSkipNoInterests(range, commandInfo, { selectionManagerService: this._selectionManagerService });
if (resultRange && serializeRange(resultRange) === serializeRange(range)) {
return {
redos: [],
undos: [],
};
}
return {
redos: [{
id: UpdateHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id,
payload: {
payload: `#gid=${subUnitId}&range=${resultRange ? serializeRange(resultRange) : 'err'}`,
},
} as IUpdateHyperLinkMutationParams,
}],
undos: [{
id: UpdateHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id,
payload: {
payload,
},
} as IUpdateHyperLinkMutationParams,
}],
};
};
this._rangeDisableMap.set(id, this._refRangeService.registerRefRange(range, handleRangeChange, unitId, subUnitId));
if (!silent) {
this._rangeWatcherMap.set(id, this._refRangeService.watchRange(unitId, subUnitId, range, (before, after) => {
this._hyperLinkModel.updateHyperLink(unitId, subUnitId, id, {
payload: `#gid=${subUnitId}&range=${after ? serializeRange(after) : 'err'}`,
}, true);
}, true));
}
}
}
}
}
private _unregisterRange(id: string) {
const disposable = this._rangeDisableMap.get(id);
disposable?.dispose();
this._rangeDisableMap.delete(id);
}
private _unwatchRange(id: string) {
const disposable = this._rangeWatcherMap.get(id);
disposable?.dispose();
this._rangeWatcherMap.delete(id);
}
private _initData() {
const data = this._hyperLinkModel.getAll();
data.forEach((unitData) => {
unitData.forEach((subUnitData) => {
const { unitId, subUnitId, links } = subUnitData;
links.forEach((link) => {
this._registerPosition(unitId, subUnitId, link);
this._watchPosition(unitId, subUnitId, link);
this._registerRange(unitId, link.id, link.payload);
});
});
});
}
private _initRefRange() {
this.disposeWithMe(
this._hyperLinkModel.linkUpdate$.subscribe((option) => {
switch (option.type) {
case 'add': {
this._registerPosition(option.unitId, option.subUnitId, option.payload);
this._watchPosition(option.unitId, option.subUnitId, option.payload);
this._registerRange(option.unitId, option.payload.id, option.payload.payload);
break;
}
case 'remove': {
this._unregisterPosition(option.payload.id);
this._unwatchPosition(option.payload.id);
this._unregisterRange(option.payload.id);
this._unwatchRange(option.payload.id);
break;
}
case 'updateRef': {
const { unitId, subUnitId, id, silent } = option;
const link = this._hyperLinkModel.getHyperLink(unitId, subUnitId, id);
if (!link) {
return;
}
this._unregisterPosition(id);
this._registerPosition(unitId, subUnitId, link);
if (!silent) {
this._unwatchPosition(id);
this._watchPosition(unitId, subUnitId, link);
}
break;
}
case 'unload': {
const { unitLinks } = option;
unitLinks.forEach((subUnitData) => {
const { links } = subUnitData;
links.forEach((link) => {
this._unregisterPosition(link.id);
this._unwatchPosition(link.id);
this._unregisterRange(link.id);
this._unwatchRange(link.id);
});
});
break;
}
case 'update': {
if (!option.silent) {
this._unwatchRange(option.id);
}
this._unregisterRange(option.id);
this._registerRange(option.unitId, option.id, option.payload.payload, option.silent);
break;
}
}
})
);
this.disposeWithMe(toDisposable(() => {
this._disposableMap.forEach((item) => {
item.dispose();
});
this._disposableMap.clear();
}));
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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 { IRemoveSheetCommandParams } from '@univerjs/sheets';
import { Disposable, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
import { AddHyperLinkMutation } from '../commands/mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../commands/mutations/remove-hyper-link.mutation';
import { HyperLinkModel } from '../models/hyper-link.model';
export class SheetsHyperLinkRemoveSheetController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService,
@Inject(HyperLinkModel) private _hyperLinkModel: HyperLinkModel
) {
super();
this._initSheetChange();
}
private _initSheetChange() {
this.disposeWithMe(
this._sheetInterceptorService.interceptCommand({
getMutations: (commandInfo) => {
if (commandInfo.id === RemoveSheetCommand.id) {
const params = commandInfo.params as IRemoveSheetCommandParams;
const workbook = params.unitId ? this._univerInstanceService.getUnit<Workbook>(params.unitId) : this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return { redos: [], undos: [] };
}
const unitId = workbook.getUnitId();
const subUnitId = params.subUnitId || workbook.getActiveSheet()?.getSheetId();
if (!subUnitId) {
return { redos: [], undos: [] };
}
const links = this._hyperLinkModel.getSubUnit(unitId, subUnitId);
const redos = links.map((link) => ({
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
},
}));
const undos = links.map((link) => ({
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link,
},
}));
return { redos, undos };
}
return { redos: [], undos: [] };
},
})
);
}
}

View File

@@ -0,0 +1,252 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable, IDocumentData, Workbook } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
import type { IUpdateRichHyperLinkMutationParams } from '../commands/mutations/update-hyper-link.mutation';
import { CustomRangeType, Disposable, DisposableCollection, ICommandService, Inject, isValidRange, IUniverInstanceService, ObjectMatrix, Rectangle, UniverInstanceType } from '@univerjs/core';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import { getSheetCommandTarget, handleDefaultRangeChangeWithEffectRefCommands, RefRangeService, SetRangeValuesMutation } from '@univerjs/sheets';
import { UpdateRichHyperLinkMutation } from '../commands/mutations/update-hyper-link.mutation';
import { ERROR_RANGE } from '../types/const';
export class SheetsHyperLinkRichTextRefRangeController extends Disposable {
private _refRangeMap: Map<string, Map<string, ObjectMatrix<IDisposable>>> = new Map();
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService
) {
super();
this._initWorkbookLoad();
this._initWorkbookUnload();
this._initSetRangesListener();
}
private _enusreMap(unitId: string, subUnitId: string) {
let unitMap = this._refRangeMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._refRangeMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new ObjectMatrix();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
private _isLegalRangeUrl(unitId: string, payload: string) {
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return null;
}
if (payload && payload.startsWith('#')) {
const search = new URLSearchParams(payload.slice(1));
// range, gid, rangeid
const searchObj = {
gid: search.get('gid') ?? '',
range: search.get('range') ?? '',
rangeid: search.get('rangeid') ?? '',
};
if (searchObj.range && searchObj.gid) {
const subUnitId = searchObj.gid;
const worksheet = workbook.getSheetBySheetId(subUnitId);
if (!worksheet) {
return null;
}
const range = deserializeRangeWithSheet(searchObj.range).range;
if (isValidRange(range, worksheet) && searchObj.range !== ERROR_RANGE) {
return {
range,
worksheet,
};
}
}
}
return null;
}
private _registerRange(unitId: string, subUnitId: string, row: number, col: number, p: IDocumentData) {
const map = this._enusreMap(unitId, subUnitId);
if (p.body?.customRanges?.some((customRange) => customRange.rangeType === CustomRangeType.HYPERLINK && this._isLegalRangeUrl(unitId, customRange.properties?.url))) {
const disposableCollection = new DisposableCollection();
let hasWatch = false;
p.body?.customRanges?.forEach((customRange) => {
if (customRange.rangeType === CustomRangeType.HYPERLINK) {
const payload = customRange.properties?.url;
const rangeInfo = this._isLegalRangeUrl(unitId, payload);
if (rangeInfo) {
const { range, worksheet } = rangeInfo;
hasWatch = true;
disposableCollection.add(
this._refRangeService.registerRefRange(
range,
(commandInfo) => {
const newRange = handleDefaultRangeChangeWithEffectRefCommands(range, commandInfo);
if (newRange && Rectangle.equals(newRange, range)) {
return {
preRedos: [],
preUndos: [],
redos: [],
undos: [],
};
}
return {
preRedos: [{
id: UpdateRichHyperLinkMutation.id,
params: {
unitId,
subUnitId,
row,
col,
id: customRange.rangeId,
url: `#gid=${subUnitId}&range=${newRange ? serializeRange(newRange) : ERROR_RANGE}`,
},
}],
undos: [{
id: UpdateRichHyperLinkMutation.id,
params: {
unitId,
subUnitId,
row,
col,
id: customRange.rangeId,
url: payload,
},
}],
redos: [],
};
},
worksheet.getUnitId(),
worksheet.getSheetId()
)
);
}
}
});
if (hasWatch) {
map.setValue(row, col, disposableCollection);
}
}
}
private _initWorkbookLoad() {
const handleWorkbook = (workbook: Workbook) => {
const unitId = workbook.getUnitId();
workbook.getSheets().forEach((sheet) => {
const subUnitId = sheet.getSheetId();
const map = this._enusreMap(unitId, subUnitId);
sheet.getCellMatrix().forValue((row, col, cell) => {
const dispose = map.getValue(row, col);
if (dispose) {
dispose.dispose();
}
if (cell && cell.p) {
this._registerRange(unitId, subUnitId, row, col, cell.p);
}
});
});
};
this._univerInstanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET).forEach((workbook) => {
handleWorkbook(workbook);
});
this.disposeWithMe(
this._univerInstanceService.unitAdded$.subscribe((event) => {
const { unit } = event;
if (unit.type === UniverInstanceType.UNIVER_SHEET) {
const workbook = unit as Workbook;
handleWorkbook(workbook);
}
})
);
}
private _initWorkbookUnload() {
this.disposeWithMe(
this._univerInstanceService.unitDisposed$.subscribe((unit) => {
if (unit.type === UniverInstanceType.UNIVER_SHEET) {
const workbook = unit as Workbook;
const unitId = workbook.getUnitId();
workbook.getSheets().forEach((sheet) => {
const subUnitId = sheet.getSheetId();
const map = this._enusreMap(unitId, subUnitId);
map.forValue((row, col, dispose) => {
if (dispose) {
dispose.dispose();
}
});
});
this._refRangeMap.delete(unitId);
}
})
);
}
private _initSetRangesListener() {
this.disposeWithMe(
this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetRangeValuesMutation.id) {
const params = commandInfo.params as ISetRangeValuesMutationParams;
const { unitId, subUnitId, cellValue } = params;
const map = this._enusreMap(unitId, subUnitId);
if (cellValue) {
new ObjectMatrix(cellValue).forValue((row, col, cell) => {
const dispose = map.getValue(row, col);
if (dispose) {
dispose.dispose();
}
if (cell && cell.p) {
this._registerRange(unitId, subUnitId, row, col, cell.p);
}
});
}
}
})
);
this.disposeWithMe(
this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === UpdateRichHyperLinkMutation.id) {
const params = commandInfo.params as IUpdateRichHyperLinkMutationParams;
const { unitId, subUnitId, row, col } = params;
const sheetTarget = getSheetCommandTarget(this._univerInstanceService, { unitId, subUnitId });
const map = this._enusreMap(unitId, subUnitId);
const dispose = map.getValue(row, col);
if (dispose) {
dispose.dispose();
}
if (sheetTarget) {
const { worksheet } = sheetTarget;
const cell = worksheet.getCellRaw(row, col);
if (cell && cell.p) {
this._registerRange(unitId, subUnitId, row, col, cell.p);
}
}
}
})
);
}
}

View File

@@ -0,0 +1,209 @@
/**
* 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, Workbook } from '@univerjs/core';
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
import { BuildTextUtils, CustomRangeType, Disposable, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, generateRandomId, Inject, IUniverInstanceService, ObjectMatrix, Range, TextX, Tools, UniverInstanceType } from '@univerjs/core';
import { AFTER_CELL_EDIT, ClearSelectionAllCommand, ClearSelectionContentCommand, ClearSelectionFormatCommand, getSheetCommandTarget, SetRangeValuesCommand, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
import { AddHyperLinkMutation } from '../commands/mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../commands/mutations/remove-hyper-link.mutation';
import { HyperLinkModel } from '../models/hyper-link.model';
export class SheetHyperLinkSetRangeController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(HyperLinkModel) private readonly _hyperLinkModel: HyperLinkModel,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._initCommandInterceptor();
this._initAfterEditor();
}
private _initCommandInterceptor() {
this._initSetRangeValuesCommandInterceptor();
this._initClearSelectionCommandInterceptor();
}
private _initSetRangeValuesCommandInterceptor() {
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
getMutations: (command) => {
if (command.id === SetRangeValuesCommand.id) {
const params = command.params as ISetRangeValuesMutationParams;
const { unitId, subUnitId } = params;
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
if (params.cellValue) {
new ObjectMatrix(params.cellValue).forValue((row, col) => {
const link = this._hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, col);
if (link) {
// rich-text can store link in custom-range, don't save to link model
redos.push({
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
},
});
undos.push({
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link,
},
});
}
});
}
return {
undos,
redos,
};
}
return {
redos: [],
undos: [],
};
},
}));
}
private _initClearSelectionCommandInterceptor() {
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
getMutations: (command) => {
if (
command.id === ClearSelectionContentCommand.id ||
command.id === ClearSelectionAllCommand.id ||
command.id === ClearSelectionFormatCommand.id
) {
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
const selection = this._selectionManagerService.getCurrentLastSelection();
const target = getSheetCommandTarget(this._univerInstanceService, command.params);
if (selection && target) {
const { unitId, subUnitId } = target;
Range.foreach(selection.range, (row, col) => {
const link = this._hyperLinkModel.getHyperLinkByLocation(unitId, subUnitId, row, col);
if (link) {
redos.push({
id: RemoveHyperLinkMutation.id,
params: {
unitId,
subUnitId,
id: link.id,
},
});
undos.push({
id: AddHyperLinkMutation.id,
params: {
unitId,
subUnitId,
link,
},
});
}
});
}
return {
redos,
undos,
};
}
return {
redos: [],
undos: [],
};
},
}));
}
private _initAfterEditor() {
this.disposeWithMe(this._sheetInterceptorService.writeCellInterceptor.intercept(AFTER_CELL_EDIT, {
handler: (cell, context, next) => {
if (!cell || cell.p) {
return next(cell);
}
if (typeof cell.v === 'string' && Tools.isLegalUrl(cell.v) && cell.v[cell.v.length - 1] !== ' ') {
const { unitId, subUnitId, row, col } = context;
const link = Tools.normalizeUrl(cell.v);
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getSheetBySheetId(subUnitId);
if (!worksheet) {
return next(cell);
}
// const renderer = this._renderManagerService.getRenderUnitById(unitId);
// const skeleton = renderer?.with(SheetSkeletonManagerService).getSkeletonParam(subUnitId);
// if (!skeleton) {
// return next(cell);
// }
const doc = worksheet.getBlankCellDocumentModel(cell, row, col);
if (!doc.documentModel) {
return next(cell);
}
const textX = BuildTextUtils.selection.replace({
selection: {
startOffset: 0,
endOffset: cell.v.length,
collapsed: false,
},
body: {
dataStream: `${cell.v}`,
customRanges: [{
startIndex: 0,
endIndex: cell.v.length - 1,
rangeId: generateRandomId(),
rangeType: CustomRangeType.HYPERLINK,
properties: {
url: link,
},
}],
},
doc: doc.documentModel,
});
if (!textX) {
return next(cell);
}
const body = doc.documentModel.getBody()!;
TextX.apply(body, textX.serialize());
return next({
...cell,
p: {
id: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
body,
documentStyle: {
pageSize: {
width: Infinity,
height: Infinity,
},
},
},
});
}
return next(cell);
},
}));
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, ICommandService } from '@univerjs/core';
import { AddHyperLinkCommand, AddRichHyperLinkCommand } from '../commands/commands/add-hyper-link.command';
import { CancelHyperLinkCommand, CancelRichHyperLinkCommand } from '../commands/commands/remove-hyper-link.command';
import { UpdateHyperLinkCommand, UpdateRichHyperLinkCommand } from '../commands/commands/update-hyper-link.command';
import { AddHyperLinkMutation } from '../commands/mutations/add-hyper-link.mutation';
import { RemoveHyperLinkMutation } from '../commands/mutations/remove-hyper-link.mutation';
import { UpdateHyperLinkMutation, UpdateHyperLinkRefMutation, UpdateRichHyperLinkMutation } from '../commands/mutations/update-hyper-link.mutation';
export class SheetsHyperLinkController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._registerCommands();
}
private _registerCommands() {
[
AddHyperLinkCommand,
UpdateHyperLinkCommand,
CancelHyperLinkCommand,
UpdateRichHyperLinkCommand,
CancelRichHyperLinkCommand,
AddRichHyperLinkCommand,
AddHyperLinkMutation,
UpdateHyperLinkMutation,
RemoveHyperLinkMutation,
UpdateHyperLinkRefMutation,
UpdateRichHyperLinkMutation,
].forEach((command) => {
this._commandService.registerCommand(command);
});
}
}

View File

@@ -0,0 +1,148 @@
/**
* 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 { IWorkbookData, UnitModel } from '@univerjs/core';
import { ILogService, IUniverInstanceService, LocaleType, LogLevel, Univer, UniverInstanceType } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { DocSelectionManagerService } from '@univerjs/docs';
import { IDefinedNamesService } from '@univerjs/engine-formula';
import { SheetHyperLinkType, UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import '@univerjs/sheets/facade';
import '../index';
function createWorkbookData(): IWorkbookData {
return {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: 'Workbook',
sheetOrder: ['sheet1'],
styles: {},
sheets: {
sheet1: {
id: 'sheet1',
name: 'Sheet1',
cellData: {},
rowCount: 20,
columnCount: 20,
},
},
};
}
describe('FRangeSheetsHyperlinkMixin', () => {
let univer: Univer;
let univerAPI: FUniver;
beforeEach(() => {
univer = new Univer();
univer.registerPlugin(UniverSheetsHyperLinkPlugin);
const injector = univer.__getInjector();
injector.add([DocSelectionManagerService]);
const workbook = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
injector.get(IUniverInstanceService).focusUnit(workbook.getUnitId());
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
univerAPI = FUniver.newAPI(injector);
});
afterEach(() => {
univer.dispose();
});
it('should set, read, update and cancel hyperlinks through the real facade API', async () => {
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
const a1 = worksheet.getRange('A1');
const b2 = worksheet.getRange('B2');
const events: string[] = [];
const addDisposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetLinkAdd, (event) => {
events.push(`add:${event.row}:${event.col}:${event.link.payload}`);
});
const updateDisposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetLinkUpdate, (event) => {
events.push(`update:${event.row}:${event.column}:${event.payload.payload}`);
});
const cancelDisposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetLinkCancel, (event) => {
events.push(`cancel:${event.row}:${event.column}`);
});
await expect(a1.setHyperLink('https://univer.ai', 'Univer')).resolves.toBe(true);
await expect(b2.setHyperLink('https://docs.univer.ai', 'Docs')).resolves.toBe(true);
expect(worksheet.getRange('A1:B2').getHyperLinks()).toEqual([
{
id: expect.any(String),
row: 0,
column: 0,
url: 'https://univer.ai',
label: 'Univer',
},
{
id: expect.any(String),
row: 1,
column: 1,
url: 'https://docs.univer.ai',
label: 'Docs',
},
]);
await expect(a1.updateHyperLink('https://univer.ai/docs', 'Univer Docs')).resolves.toBe(true);
expect(a1.getHyperLinks()).toEqual([{
id: expect.any(String),
row: 0,
column: 0,
url: 'https://univer.ai/docs',
label: 'Univer Docs',
}]);
expect(worksheet.getRange('A1:B2').cancelHyperLink()).toBe(true);
expect(worksheet.getRange('A1:B2').getHyperLinks()).toEqual([]);
expect(events).toEqual([
'add:0:0:https://univer.ai',
'add:1:1:https://docs.univer.ai',
'update:0:0:https://univer.ai/docs',
'cancel:0:0',
'cancel:1:1',
]);
addDisposable.dispose();
updateDisposable.dispose();
cancelDisposable.dispose();
});
it('should create hyperlink urls from sheet ranges', () => {
const workbook = univerAPI.getActiveWorkbook()!;
const worksheet = workbook.getActiveSheet();
expect(worksheet.getUrl()).toBe('#gid=sheet1');
expect(worksheet.getRange('A1:B2').getUrl()).toBe('#gid=sheet1&range=A1:B2');
univer.__getInjector().get(IDefinedNamesService).registerDefinedName(workbook.getId(), {
id: 'range-1',
name: 'Total',
formulaOrRefString: 'Sheet1!A1:B2',
});
expect(workbook.getUrlOfDefineName('Total')).toBe('#rangeid=range-1');
expect(workbook.parseSheetHyperlink('https://univer.ai')).toEqual({
type: SheetHyperLinkType.URL,
name: 'https://univer.ai',
url: 'https://univer.ai',
searchObj: null,
});
});
});

View File

@@ -0,0 +1,152 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IEventBase } from '@univerjs/core/facade';
import type { ICellLinkContent, ISheetHyperLink } from '@univerjs/sheets-hyper-link';
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
import { FEventName } from '@univerjs/core/facade';
/**
* @ignore
*/
interface IFSheetsHyperlinkEventNameMixin {
/**
* Event triggered before adding a link
* @see {@link IBeforeSheetLinkAddEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetLinkAdd, (params) => {
* const { workbook, worksheet, row, col, link } = params;
* console.log('before sheet link add', params);
*
* // Cancel the sheet link add operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetLinkAdd: 'BeforeSheetLinkAdd';
/**
* Event triggered before canceling a link
* @see {@link IBeforeSheetLinkCancelEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetLinkCancel, (params) => {
* const { workbook, worksheet, row, column, id } = params;
* console.log('before sheet link cancel', params);
*
* // Cancel the sheet link cancel operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetLinkCancel: 'BeforeSheetLinkCancel';
/**
* Event triggered before updating a link
* @see {@link IBeforeSheetLinkUpdateEventParams}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetLinkUpdate, (params) => {
* const { workbook, worksheet, row, column, id, payload } = params;
* console.log('before sheet link update', params);
*
* // Cancel the sheet link update operation
* params.cancel = true;
* });
*
* // Remove the event listener, use `disposable.dispose()`
* ```
*/
readonly BeforeSheetLinkUpdate: 'BeforeSheetLinkUpdate';
}
export class FSheetsHyperlinkEventNameMixin extends FEventName implements IFSheetsHyperlinkEventNameMixin {
override get BeforeSheetLinkAdd(): 'BeforeSheetLinkAdd' {
return 'BeforeSheetLinkAdd' as const;
}
override get BeforeSheetLinkCancel(): 'BeforeSheetLinkCancel' {
return 'BeforeSheetLinkCancel' as const;
}
override get BeforeSheetLinkUpdate(): 'BeforeSheetLinkUpdate' {
return 'BeforeSheetLinkUpdate' as const;
}
}
export interface IBeforeSheetLinkAddEventParams extends IEventBase {
/** The workbook instance */
workbook: FWorkbook;
/** The worksheet where the link will be added */
worksheet: FWorksheet;
/** The row index of the target cell */
row: number;
/** The column index of the target cell */
col: number;
/** The hyperlink information to be added */
link: ISheetHyperLink;
}
export interface IBeforeSheetLinkCancelEventParams extends IEventBase {
/** The workbook instance */
workbook: FWorkbook;
/** The worksheet containing the link */
worksheet: FWorksheet;
/** The row index of the cell */
row: number;
/** The column index of the cell */
column: number;
/** The unique identifier of the hyperlink */
id: string;
}
export interface IBeforeSheetLinkUpdateEventParams extends IEventBase {
/** The workbook instance */
workbook: FWorkbook;
/** The worksheet containing the link */
worksheet: FWorksheet;
/** The row index of the cell */
row: number;
/** The column index of the cell */
column: number;
/** The unique identifier of the hyperlink */
id: string;
/** The new hyperlink content/information */
payload: ICellLinkContent;
}
/**
* @ignore
*/
export interface ISheetsHyperlinkEventParamConfig {
BeforeSheetLinkAdd: IBeforeSheetLinkAddEventParams;
BeforeSheetLinkCancel: IBeforeSheetLinkCancelEventParams;
BeforeSheetLinkUpdate: IBeforeSheetLinkUpdateEventParams;
}
FEventName.extend(FSheetsHyperlinkEventNameMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEventName extends IFSheetsHyperlinkEventNameMixin {}
interface IEventParamConfig extends ISheetsHyperlinkEventParamConfig { }
}

View File

@@ -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 { IAddHyperLinkCommandParams, ICancelHyperLinkCommandParams, IUpdateHyperLinkCommandParams } from '@univerjs/sheets-hyper-link';
import { CustomRangeType, DataStreamTreeTokenType, generateRandomId } from '@univerjs/core';
import { AddHyperLinkCommand, CancelHyperLinkCommand, SheetsHyperLinkParserService, UpdateHyperLinkCommand } from '@univerjs/sheets-hyper-link';
import { FRange } from '@univerjs/sheets/facade';
export interface ICellHyperLink {
id: string;
row: number;
column: number;
url: string;
label: string;
}
/**
* @ignore
*/
export interface IFRangeSheetsHyperlinkMixin {
/**
* Set a hyperlink for this range top left cell.
* The hyperlink can be a URL, a range link, or a sheet link.
* When the hyperlink is a range link or a sheet link, the url should be the url of the target range or sheet.
* @param {string} url - The hyperlink url, can be a URL, a range link, or a sheet link.
* @param {string} [label] - The display text of the hyperlink. If omitted, the existing cell text is linked. Supply a label when linking an empty cell.
* @return {Promise<boolean>} A promise that resolves to true if the hyperlink is set successfully, otherwise false.
*
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a hyperlink to Univer on cell A1
* const fRange = fWorksheet.getRange('A1');
* await fRange.setHyperLink('https://univer.ai/', 'Univer');
*
* // Create a hyperlink to Sheet1 range B2:D4 on cell A2
* const fRange2 = fWorksheet.getRange('A2');
* const rangeUrl = fWorksheet.getRange('B2:D4').getUrl();
* await fRange2.setHyperLink(rangeUrl, 'Link to B2:D4');
*
* // Create a hyperlink to another sheet range on cell A3
* const anotherSheet = fWorkbook.getSheetByName('Another Sheet');
* if (anotherSheet) {
* const anotherSheetUrl = anotherSheet.getUrl();
* const fRange3 = fWorksheet.getRange('A3');
* await fRange3.setHyperLink(anotherSheetUrl, 'Link to Another Sheet');
* }
*
* // Create a hyperlink to a defined name on cell A4
* const fRange4 = fWorksheet.getRange('A4');
* const definedNameHyperlinkUrl = fWorkbook.getUrlOfDefineName('MyDefinedName');
* await fRange4.setHyperLink(definedNameHyperlinkUrl, 'Link to MyDefinedName');
* ```
*/
setHyperLink(url: string, label?: string): Promise<boolean>;
/**
* Gets the first hyperlink from each cell containing hyperlinks in this range.
* @returns {ICellHyperLink[]} At most one hyperlink per cell, with absolute, zero-based worksheet coordinates.
*
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* console.log(fWorksheet.getRange('A1:T100').getHyperLinks());
* ```
*/
getHyperLinks(): ICellHyperLink[];
/**
* Update the hyperlink of this range top left cell.
* @param {string} url - The new hyperlink url, can be a URL, a range link, or a sheet link.
* @param {string} [label] - The new display text of the hyperlink. If omitted, the replacement display text is empty. Supply a label to keep the link visible.
*
* @returns {Promise<boolean>} A promise resolving to whether the update succeeded.
* @throws {Error} The promise rejects if the top-left cell contains no hyperlink.
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a hyperlink to Univer on cell A1
* const fRange = fWorksheet.getRange('A1');
* await fRange.setHyperLink('https://univer.ai/', 'Univer');
*
* // Update hyperlink after 3 seconds
* await new Promise((resolve) => setTimeout(resolve, 3000));
*
* const rangeUrl = fWorksheet.getRange('B2:D4').getUrl();
* await fRange.updateHyperLink(rangeUrl, 'Link to B2:D4');
* ```
*/
updateHyperLink(url: string, label?: string): Promise<boolean>;
/**
* Cancel all hyperlinks in this range. If a hyperlink is provided, only cancel the specified hyperlink.
* @param {ICellHyperLink} [hyperlink] - The hyperlink to be cancelled. If not provided, all hyperlinks in this range will be cancelled.
* @return {boolean} True if the hyperlink(s) is cancelled successfully, otherwise false.
*
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Cancel the hyperlink in cell A1
* const fRange = fWorksheet.getRange('A1');
* fRange.cancelHyperLink();
*
* // Cancel all hyperlinks in range A2:B4
* const fRange2 = fWorksheet.getRange('A2:B4');
* fRange2.cancelHyperLink();
*
* // Cancel a specific hyperlink in range A1:T100
* const fRange3 = fWorksheet.getRange('A1:T100');
* const hyperlinks = fRange3.getHyperLinks();
* if (hyperlinks.length > 1) {
* fRange3.cancelHyperLink(hyperlinks[1]);
* }
* ```
*/
cancelHyperLink(hyperlink?: ICellHyperLink): boolean;
/**
* Create a hyperlink url to this range
* @returns {string} The hyperlink url of this range
*
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1');
* const url = fRange.getUrl();
* console.log(url);
* ```
*/
getUrl(): string;
}
export class FRangeSheetsHyperlinkMixin extends FRange implements IFRangeSheetsHyperlinkMixin {
override setHyperLink(url: string, label?: string): Promise<boolean> {
return this._commandService.executeCommand<IAddHyperLinkCommandParams>(AddHyperLinkCommand.id, {
unitId: this.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
link: {
id: generateRandomId(),
row: this._range.startRow,
column: this._range.startColumn,
payload: url,
display: label,
},
});
}
override getHyperLinks(): ICellHyperLink[] {
const hyperlinks: ICellHyperLink[] = [];
this.forEach((row, column, cell) => {
if (!cell.p) {
return;
}
const ranges = cell.p.body?.customRanges?.filter((range) => range.rangeType === CustomRangeType.HYPERLINK) ?? [];
if (ranges.length > 0) {
const dataStream = cell.p?.body?.dataStream;
const { rangeId, properties, startIndex, endIndex } = ranges[0];
const url = properties?.url ?? '';
const label = dataStream?.slice(startIndex, endIndex + 1).replaceAll(DataStreamTreeTokenType.CUSTOM_RANGE_START, '').replaceAll(DataStreamTreeTokenType.CUSTOM_RANGE_END, '') ?? '';
hyperlinks.push({
id: `${rangeId}`,
row,
column,
url,
label,
});
}
});
return hyperlinks;
}
override updateHyperLink(url: string, label?: string): Promise<boolean> {
const hyperlink = this.getHyperLinks().find((link) => link.row === this._range.startRow && link.column === this._range.startColumn);
if (!hyperlink) {
return Promise.reject(new Error('No hyperlink found in the top left cell of the range'));
}
const { id, row, column } = hyperlink;
return this._commandService.executeCommand<IUpdateHyperLinkCommandParams>(UpdateHyperLinkCommand.id, {
unitId: this.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
id,
row,
column,
payload: {
payload: url,
display: label,
},
});
}
override cancelHyperLink(hyperlink?: ICellHyperLink): boolean {
if (hyperlink) {
const { id, row, column } = hyperlink;
return this._commandService.syncExecuteCommand<ICancelHyperLinkCommandParams>(CancelHyperLinkCommand.id, {
unitId: this.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
id,
row,
column,
});
} else {
const hyperlinks = this.getHyperLinks();
if (hyperlinks.length === 0) {
return true;
}
return hyperlinks.every((link) => {
const { id, row, column } = link;
return this._commandService.syncExecuteCommand<ICancelHyperLinkCommandParams>(CancelHyperLinkCommand.id, {
unitId: this.getUnitId(),
subUnitId: this._worksheet.getSheetId(),
id,
row,
column,
});
});
}
}
override getUrl(): string {
const parserService = this._injector.get(SheetsHyperLinkParserService);
return parserService.buildHyperLink(this.getRange(), this.getSheetId());
}
}
FRange.extend(FRangeSheetsHyperlinkMixin);
declare module '@univerjs/sheets/facade' {
interface FRange extends IFRangeSheetsHyperlinkMixin {}
}

View File

@@ -0,0 +1,123 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Injector } from '@univerjs/core';
import type { IAddHyperLinkCommandParams, ICancelHyperLinkCommandParams, IUpdateHyperLinkCommandParams } from '@univerjs/sheets-hyper-link';
import type { IBeforeSheetLinkAddEventParams, IBeforeSheetLinkCancelEventParams, IBeforeSheetLinkUpdateEventParams } from './f-event';
import { CanceledError, ICommandService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { AddHyperLinkCommand, CancelHyperLinkCommand, UpdateHyperLinkCommand } from '@univerjs/sheets-hyper-link';
export class FUniverSheetsHyperlinkMixin extends FUniver {
/**
* @ignore
*/
// eslint-disable-next-line max-lines-per-function
override _initialize(injector: Injector): void {
const commandService = injector.get(ICommandService);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetLinkAdd,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== AddHyperLinkCommand.id) return;
const params = commandInfo.params as IAddHyperLinkCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { link } = params;
const { row, column: col } = link;
const eventParams: IBeforeSheetLinkAddEventParams = {
workbook,
worksheet,
row,
col,
link,
};
this.fireEvent(this.Event.BeforeSheetLinkAdd, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetLinkUpdate,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== UpdateHyperLinkCommand.id) return;
const params = commandInfo.params as IUpdateHyperLinkCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { row, column, id, payload } = params;
const eventParams: IBeforeSheetLinkUpdateEventParams = {
workbook,
worksheet,
row,
column,
id,
payload,
};
this.fireEvent(this.Event.BeforeSheetLinkUpdate, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
})
)
);
this.disposeWithMe(
this.registerEventHandler(
this.Event.BeforeSheetLinkCancel,
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== CancelHyperLinkCommand.id) return;
const params = commandInfo.params as ICancelHyperLinkCommandParams;
const target = this.getSheetCommandTarget(params);
if (!target) return;
const { workbook, worksheet } = target;
const { row, column, id } = params;
const eventParams: IBeforeSheetLinkCancelEventParams = {
workbook,
worksheet,
row,
column,
id,
};
this.fireEvent(this.Event.BeforeSheetLinkCancel, eventParams);
if (eventParams.cancel) {
throw new CanceledError();
}
})
)
);
}
}
FUniver.extend(FUniverSheetsHyperlinkMixin);

View File

@@ -0,0 +1,111 @@
/**
* 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 { ISheetHyperLinkInfo } from '@univerjs/sheets-hyper-link';
import { IDefinedNamesService, operatorToken } from '@univerjs/engine-formula';
import { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link';
import { FWorkbook } from '@univerjs/sheets/facade';
/**
* @ignore
*/
export interface IFWorkbookHyperlinkMixin {
/**
* Create a hyperlink url for the defined name.
* The defined name must exist in the current workbook and must be a reference to a range, otherwise an error will be thrown.
* @param {string} name - The name of the defined name.
* @returns {string} The hyperlink url of the defined name.
*
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
*
* // Create a defined name "TestRange" for the range A1:B10 of the active sheet
* const definedNameParam = fWorkbook.newDefinedNameBuilder()
* .setName('TestRange')
* .setRef('Sheet1!$A$1:$B$10')
* .build();
* fWorkbook.insertDefinedNameBuilder(definedNameParam);
*
* // Create a hyperlink to the defined name "TestRange" on cell C1
* const url = fWorkbook.getUrlOfDefineName('TestRange');
* console.log(url);
* const fRange = fWorksheet.getRange('C1');
* fRange.setHyperLink(url, 'Link to TestRange');
*
* // Create a hyperlink to the exiting defined name on cell C2
* const definedNames = fWorkbook.getDefinedNames();
* console.log(definedNames);
* const exitsDefinedNameUrl = fWorkbook.getUrlOfDefineName(definedNames[0].getName());
* console.log(exitsDefinedNameUrl);
* const fRange2 = fWorksheet.getRange('C2');
* fRange2.setHyperLink(exitsDefinedNameUrl, `Link to ${definedNames[0].getName()}`);
* ```
*/
getUrlOfDefineName(name: string): string;
/**
* Parse the hyperlink string to get the hyperlink info.
* @param {string} hyperlink - The hyperlink string.
* @returns {ISheetHyperLinkInfo} The hyperlink info.
*
* @example
* ``` ts
* // Create a hyperlink to the range A1:D10 of the current sheet
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const fRange = fWorksheet.getRange('A1:D10');
* const hyperlink = fRange.getUrl();
*
* // Parse the hyperlink
* const hyperlinkInfo = fWorkbook.parseSheetHyperlink(hyperlink);
* console.log(hyperlinkInfo);
* ```
*/
parseSheetHyperlink(hyperlink: string): ISheetHyperLinkInfo;
}
export class FWorkbookHyperlinkMixin extends FWorkbook implements IFWorkbookHyperlinkMixin {
override getUrlOfDefineName(name: string): string {
const definedNameService = this._injector.get(IDefinedNamesService);
const parserService = this._injector.get(SheetsHyperLinkParserService);
const definedName = definedNameService.getValueByName(this._workbook.getUnitId(), name);
if (!definedName) {
throw new Error(`Defined name "${name}" does not exist.`);
}
if (definedName.formulaOrRefString.startsWith(operatorToken.EQUALS)) {
throw new Error(`Defined name "${name}" is not a reference range, cannot be converted to hyperlink.`);
}
return parserService.buildHyperLink(definedName.id);
}
override parseSheetHyperlink(hyperlink: string): ISheetHyperLinkInfo {
const parserService = this._injector.get(SheetsHyperLinkParserService);
return parserService.parseHyperLink(hyperlink);
}
}
FWorkbook.extend(FWorkbookHyperlinkMixin);
declare module '@univerjs/sheets/facade' {
// eslint-disable-next-line ts/naming-convention
interface FWorkbook extends IFWorkbookHyperlinkMixin { }
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SheetsHyperLinkParserService } from '@univerjs/sheets-hyper-link';
import { FWorksheet } from '@univerjs/sheets/facade';
/**
* @ignore
*/
export interface IFWorksheetHyperlinkMixin {
/**
* Create a hyperlink url to this sheet
* @returns {string} The hyperlink url of this sheet
* @example
* ```ts
* const fWorkbook = univerAPI.getActiveWorkbook();
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
* if (!fWorksheet) return;
* const url = fWorksheet.getUrl();
* console.log(url);
* ```
*/
getUrl(): string;
}
export class FWorksheetHyperlinkMixin extends FWorksheet implements IFWorksheetHyperlinkMixin {
override getUrl(): string {
const parserService = this._injector.get(SheetsHyperLinkParserService);
return parserService.buildHyperLink(null, this._worksheet.getSheetId());
}
}
FWorksheet.extend(FWorksheetHyperlinkMixin);
declare module '@univerjs/sheets/facade' {
// eslint-disable-next-line ts/naming-convention
interface FWorksheet extends IFWorksheetHyperlinkMixin {}
}

View File

@@ -0,0 +1,28 @@
/**
* 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';
import './f-worksheet';
import './f-range';
import './f-event';
import './f-univer';
export type * from './f-event';
export type * from './f-range';
export type * from './f-univer';
export { FWorkbookHyperlinkMixin } from './f-workbook';
export type * from './f-workbook';
export type * from './f-worksheet';

View File

@@ -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.
*/
export { AddHyperLinkCommand, AddRichHyperLinkCommand } from './commands/commands/add-hyper-link.command';
export type {
IAddHyperLinkCommandParams,
IAddRichHyperLinkCommandParams,
} from './commands/commands/add-hyper-link.command';
export { CancelHyperLinkCommand, CancelRichHyperLinkCommand } from './commands/commands/remove-hyper-link.command';
export type {
ICancelHyperLinkCommandParams,
ICancelRichHyperLinkCommandParams,
} from './commands/commands/remove-hyper-link.command';
export { UpdateHyperLinkCommand, UpdateRichHyperLinkCommand } from './commands/commands/update-hyper-link.command';
export type {
IUpdateHyperLinkCommandParams,
IUpdateRichHyperLinkCommandParams,
} from './commands/commands/update-hyper-link.command';
export { AddHyperLinkMutation } from './commands/mutations/add-hyper-link.mutation';
export type { IAddHyperLinkMutationParams } from './commands/mutations/add-hyper-link.mutation';
export { RemoveHyperLinkMutation } from './commands/mutations/remove-hyper-link.mutation';
export type { IRemoveHyperLinkMutationParams } from './commands/mutations/remove-hyper-link.mutation';
export { UpdateHyperLinkMutation, UpdateHyperLinkRefMutation } from './commands/mutations/update-hyper-link.mutation';
export type {
IUpdateHyperLinkMutationParams,
IUpdateHyperLinkRefMutationParams,
} from './commands/mutations/update-hyper-link.mutation';
export type { IUniverSheetsHyperLinkConfig } from './config/config';
export { HyperLinkModel } from './models/hyper-link.model';
export { UniverSheetsHyperLinkPlugin } from './plugin';
export { SheetsHyperLinkParserService } from './services/parser.service';
export type { ISheetHyperLinkInfo, ISheetUrlParams } from './services/parser.service';
export { ERROR_RANGE } from './types/const';
export { SheetHyperLinkType } from './types/enums/hyper-link-type';
export type { ICellLinkContent, ISheetHyperLink } from './types/interfaces/i-hyper-link';

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'نطاق غير صالح',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Interval no vàlid',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Ungültiger Bereich',
},
},
};
export default locale;

View File

@@ -0,0 +1,25 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const locale = {
'sheets-hyper-link': {
message: {
refError: 'Invalid Range',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Rango no válido',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'محدوده نامعتبر',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Plage invalide',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Rentang Tidak Valid',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Intervallo non valido',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: '無効な範囲です',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: '잘못된 범위입니다',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Nieprawidłowy zakres',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Intervalo Inválido',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Недопустимый диапазон',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Neplatný rozsah',
},
},
};
export default locale;

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { LocaleLeafKeys } from '@univerjs/core';
import type enUS from './en-US';
export type LocaleKey = LocaleLeafKeys<typeof enUS>;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: 'Tham chiếu lỗi',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: '错误的引用',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: '錯誤的引用',
},
},
};
export default locale;

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type enUS from './en-US';
const locale: typeof enUS = {
'sheets-hyper-link': {
message: {
refError: '錯誤的引用',
},
},
};
export default locale;

View File

@@ -0,0 +1,171 @@
/**
* 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 { IWorkbookData, Workbook } from '@univerjs/core';
import type { ISheetHyperLink } from '../../types/interfaces/i-hyper-link';
import { IUniverInstanceService, LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { HyperLinkModel } from '../hyper-link.model';
function createWorkbookData(unitId = 'book-1', sheetId = 'sheet-1'): IWorkbookData {
return {
id: unitId,
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: 'Workbook',
sheetOrder: [sheetId],
styles: {},
sheets: {
[sheetId]: {
id: sheetId,
name: 'Sheet1',
cellData: {
0: {
0: { v: 'Display From Cell' },
1: { v: 'Other Cell' },
},
1: {
1: { v: 'Moved Cell' },
},
},
rowCount: 20,
columnCount: 20,
},
},
};
}
function createLink(id: string, row: number, column: number, payload = 'https://univer.ai'): ISheetHyperLink {
return {
id,
row,
column,
payload,
display: `${id}-display`,
};
}
describe('HyperLinkModel', () => {
let univer: Univer;
let model: HyperLinkModel;
beforeEach(() => {
univer = new Univer();
const injector = univer.__getInjector();
injector.add([HyperLinkModel]);
univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
injector.get(IUniverInstanceService).focusUnit('book-1');
model = injector.get(HyperLinkModel);
});
afterEach(() => {
univer.dispose();
});
it('should add, update, remove and unload hyperlinks while emitting update events', () => {
const link = createLink('link-1', 0, 0);
const events: string[] = [];
model.linkUpdate$.subscribe((event) => {
events.push(event.type);
});
expect(model.addHyperLink('book-1', 'sheet-1', link)).toBe(true);
expect(model.getHyperLink('book-1', 'sheet-1', 'link-1')).toEqual(link);
expect(model.updateHyperLink('book-1', 'sheet-1', 'link-1', { payload: 'https://docs.univer.ai' }, true)).toBe(true);
expect(model.getHyperLink('book-1', 'sheet-1', 'link-1')?.payload).toBe('https://docs.univer.ai');
expect(model.removeHyperLink('book-1', 'sheet-1', 'link-1')).toBe(true);
expect(model.removeHyperLink('book-1', 'sheet-1', 'missing')).toBe(false);
model.deleteUnit('book-1');
expect(events).toEqual(['add', 'update', 'remove', 'unload']);
expect(model.getUnit('book-1')).toEqual([]);
expect(model.getAll()).toEqual([]);
});
it('should move hyperlink references and clear the original matrix position when the original link is still there', () => {
const link = createLink('link-1', 0, 0);
model.addHyperLink('book-1', 'sheet-1', link);
expect(model.updateHyperLinkRef('book-1', 'sheet-1', 'link-1', { row: 1, column: 1 }, true)).toBe(true);
expect(model.getHyperLinkByLocation('book-1', 'sheet-1', 0, 0)).toBeUndefined();
expect(model.getHyperLinkByLocation('book-1', 'sheet-1', 1, 1)).toEqual({
...link,
row: 1,
column: 1,
});
});
it('should move hyperlink references from the cached position when the matrix slot has been overwritten', () => {
const original = createLink('original', 0, 0, 'https://origin');
const overwritten = createLink('overwritten', 0, 0, 'https://other');
model.addHyperLink('book-1', 'sheet-1', original);
model.addHyperLink('book-1', 'sheet-1', overwritten);
model.updateHyperLinkRef('book-1', 'sheet-1', 'original', { row: 1, column: 1 });
expect(model.getHyperLink('book-1', 'sheet-1', 'original')).toEqual({
...original,
row: 1,
column: 1,
});
expect(model.getHyperLinkByLocation('book-1', 'sheet-1', 0, 0)).toEqual(overwritten);
});
it('should resolve display text from worksheet cells when reading synchronously by location', () => {
const link = createLink('link-1', 0, 0);
model.addHyperLink('book-1', 'sheet-1', link);
expect(model.getHyperLinkByLocationSync('book-1', 'sheet-1', 0, 0)).toEqual({
...link,
display: 'Display From Cell',
});
expect(model.getHyperLinkByLocationSync('book-1', 'sheet-1', 9, 9)).toBeUndefined();
});
it('should expose links by sub unit and whole unit', () => {
const first = createLink('link-1', 0, 0);
const second = createLink('link-2', 1, 1);
model.addHyperLink('book-1', 'sheet-1', first);
model.addHyperLink('book-1', 'sheet-1', second);
expect(model.getSubUnit('book-1', 'sheet-1')).toEqual([first, second]);
expect(model.getUnit('book-1')).toEqual([
{
unitId: 'book-1',
subUnitId: 'sheet-1',
links: [first, second],
},
]);
expect(model.getAll()).toEqual([
[
{
unitId: 'book-1',
subUnitId: 'sheet-1',
links: [first, second],
},
],
]);
});
});

View File

@@ -0,0 +1,277 @@
/**
* 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 { ICellLinkContent, ISheetHyperLink } from '../types/interfaces/i-hyper-link';
import { Disposable, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import { Subject } from 'rxjs';
type LinkUpdate = {
type: 'add';
payload: ISheetHyperLink;
unitId: string;
subUnitId: string;
silent?: boolean;
} | {
type: 'remove';
payload: ISheetHyperLink;
unitId: string;
subUnitId: string;
silent?: boolean;
} | {
type: 'update';
unitId: string;
subUnitId: string;
payload: ICellLinkContent;
id: string;
silent?: boolean;
} | {
type: 'updateRef';
unitId: string;
subUnitId: string;
id: string;
payload: { row: number; column: number };
silent?: boolean;
} | {
type: 'unload';
unitId: string;
unitLinks: {
unitId: string;
subUnitId: string;
links: ISheetHyperLink[];
}[];
silent?: boolean;
};
export class HyperLinkModel extends Disposable {
private _linkUpdate$ = new Subject<LinkUpdate>();
linkUpdate$ = this._linkUpdate$.asObservable();
private _linkMap: Map<string, Map<string, ObjectMatrix<ISheetHyperLink>>> = new Map();
private _linkPositionMap: Map<string, Map<string, Map<string, { row: number; column: number; link: ISheetHyperLink }>>> = new Map();
constructor(
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this.disposeWithMe({
dispose: () => {
this._linkUpdate$.complete();
},
});
}
private _ensureMap(unitId: string, subUnitId: string) {
let unitMap = this._linkMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._linkMap.set(unitId, unitMap);
}
let matrix = unitMap.get(subUnitId);
if (!matrix) {
matrix = new ObjectMatrix();
unitMap.set(subUnitId, matrix);
}
let positionUnitMap = this._linkPositionMap.get(unitId);
if (!positionUnitMap) {
positionUnitMap = new Map();
this._linkPositionMap.set(unitId, positionUnitMap);
}
let positionSubUnitMap = positionUnitMap.get(subUnitId);
if (!positionSubUnitMap) {
positionSubUnitMap = new Map();
positionUnitMap.set(subUnitId, positionSubUnitMap);
}
return {
matrix,
positionMap: positionSubUnitMap,
};
}
addHyperLink(unitId: string, subUnitId: string, link: ISheetHyperLink) {
const { matrix, positionMap } = this._ensureMap(unitId, subUnitId);
matrix.setValue(link.row, link.column, link);
positionMap.set(link.id, { row: link.row, column: link.column, link });
this._linkUpdate$.next({
unitId,
subUnitId,
payload: link,
type: 'add',
});
return true;
}
updateHyperLink(
unitId: string,
subUnitId: string,
id: string,
payload: Partial<ICellLinkContent>,
silent = false
) {
const { matrix, positionMap } = this._ensureMap(unitId, subUnitId);
const position = positionMap.get(id);
if (!position) {
return true;
}
const link = matrix.getValue(position.row, position.column);
if (!link) {
return true;
}
Object.assign(link, payload);
this._linkUpdate$.next({
unitId,
subUnitId,
payload: {
display: link.display,
payload: link.payload,
},
id,
type: 'update',
silent,
});
return true;
}
updateHyperLinkRef(unitId: string, subUnitId: string, id: string, payload: { row: number; column: number }, silent = false) {
const { matrix, positionMap } = this._ensureMap(unitId, subUnitId);
const position = positionMap.get(id);
if (!position) {
return true;
}
let link = matrix.getValue(position.row, position.column);
if (!link || link.id !== id) {
link = position.link;
} else {
matrix.realDeleteValue(position.row, position.column);
}
Object.assign(link, payload);
positionMap.set(id, { ...payload, link });
matrix.setValue(payload.row, payload.column, link);
this._linkUpdate$.next({
unitId,
subUnitId,
payload,
id,
type: 'updateRef',
silent,
});
return true;
}
removeHyperLink(unitId: string, subUnitId: string, id: string) {
const { matrix, positionMap } = this._ensureMap(unitId, subUnitId);
const position = positionMap.get(id);
if (!position) {
return false;
}
positionMap.delete(id);
const link = matrix.getValue(position.row, position.column);
if (link && link.id === id) {
matrix.realDeleteValue(position.row, position.column);
}
this._linkUpdate$.next({
unitId,
subUnitId,
payload: position.link,
type: 'remove',
});
return true;
}
getHyperLink(unitId: string, subUnitId: string, id: string) {
const { matrix, positionMap } = this._ensureMap(unitId, subUnitId);
const position = positionMap.get(id);
if (!position) {
return undefined;
}
return matrix.getValue(position.row, position.column);
}
getHyperLinkByLocation(unitId: string, subUnitId: string, row: number, column: number): ISheetHyperLink | undefined {
const { matrix } = this._ensureMap(unitId, subUnitId);
return matrix.getValue(row, column)!;
}
getHyperLinkByLocationSync(unitId: string, subUnitId: string, row: number, column: number) {
const { matrix } = this._ensureMap(unitId, subUnitId);
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
const cell = workbook?.getSheetBySheetId(subUnitId)?.getCellRaw(row, column);
const cellValueStr = (cell?.v ?? cell?.p?.body?.dataStream.slice(0, -2) ?? '').toString();
const link = matrix.getValue(row, column);
if (!link) {
return undefined;
}
return {
...link,
display: cellValueStr,
};
}
getSubUnit(unitId: string, subUnitId: string) {
const { matrix } = this._ensureMap(unitId, subUnitId);
const links: ISheetHyperLink[] = [];
matrix.forValue((row, col, value) => {
if (value) {
links.push(value);
}
});
return links;
}
getUnit(unitId: string) {
const unitMap = this._linkMap.get(unitId);
if (!unitMap) {
return [];
}
return Array.from(unitMap.keys()).map((subUnitId) => {
const links = this.getSubUnit(unitId, subUnitId);
return {
unitId,
subUnitId,
links,
};
});
}
deleteUnit(unitId: string) {
const links = this.getUnit(unitId);
this._linkMap.delete(unitId);
this._linkPositionMap.delete(unitId);
this._linkUpdate$.next({
type: 'unload',
unitId,
unitLinks: links,
});
}
getAll() {
const unitIds = Array.from(this._linkMap.keys());
return unitIds.map((unitId) => this.getUnit(unitId));
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IUniverSheetsHyperLinkConfig } from './config/config';
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, registerDependencies, touchDependencies, UniverInstanceType } from '@univerjs/core';
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import pkg from '../package.json';
import { defaultPluginConfig, SHEETS_HYPER_LINK_PLUGIN_CONFIG_KEY } from './config/config';
import { SheetsHyperLinkRefRangeController } from './controllers/ref-range.controller';
import { SheetsHyperLinkRemoveSheetController } from './controllers/remove-sheet.controller';
import { SheetsHyperLinkRichTextRefRangeController } from './controllers/rich-text-ref-range.controller';
import { SheetHyperLinkSetRangeController } from './controllers/set-range.controller';
import { SheetsHyperLinkController } from './controllers/sheet-hyper-link.controller';
import { HyperLinkModel } from './models/hyper-link.model';
import { SheetsHyperLinkParserService } from './services/parser.service';
import { SHEET_HYPER_LINK_PLUGIN } from './types/const';
@DependentOn(UniverFormulaEnginePlugin, UniverSheetsPlugin)
export class UniverSheetsHyperLinkPlugin extends Plugin {
static override pluginName = SHEET_HYPER_LINK_PLUGIN;
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsHyperLinkConfig> = defaultPluginConfig,
@Inject(Injector) protected _injector: Injector,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(SHEETS_HYPER_LINK_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
registerDependencies(this._injector, [
[HyperLinkModel],
[SheetsHyperLinkParserService],
[SheetsHyperLinkController],
[SheetsHyperLinkRefRangeController],
[SheetHyperLinkSetRangeController],
[SheetsHyperLinkRemoveSheetController],
[SheetsHyperLinkRichTextRefRangeController],
]);
touchDependencies(this._injector, [
[SheetsHyperLinkRefRangeController],
[SheetsHyperLinkController],
[SheetHyperLinkSetRangeController],
[SheetsHyperLinkRemoveSheetController],
[SheetsHyperLinkRichTextRefRangeController],
]);
}
}

View File

@@ -0,0 +1,213 @@
/**
* 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, IWorkbookData, Workbook } from '@univerjs/core';
import {
IUniverInstanceService,
LocaleService,
LocaleType,
Univer,
UniverInstanceType,
} from '@univerjs/core';
import { DefinedNamesService, IDefinedNamesService } from '@univerjs/engine-formula';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { ERROR_RANGE } from '../../types/const';
import { SheetHyperLinkType } from '../../types/enums/hyper-link-type';
import { SheetsHyperLinkParserService } from '../parser.service';
function createWorkbookData(unitId = 'book-1', sheetId = 'sheet-1'): IWorkbookData {
return {
id: unitId,
appVersion: '3.0.0-alpha',
locale: LocaleType.EN_US,
name: 'Workbook',
sheetOrder: [sheetId],
styles: {},
sheets: {
[sheetId]: {
id: sheetId,
name: 'Sheet1',
cellData: {
0: {
0: { v: 'Origin' },
},
},
rowCount: 20,
columnCount: 20,
},
},
};
}
describe('SheetsHyperLinkParserService', () => {
let univer: Univer;
let service: SheetsHyperLinkParserService;
let localeService: LocaleService;
beforeEach(() => {
univer = new Univer();
const injector = univer.__getInjector();
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
injector.add([SheetsHyperLinkParserService]);
localeService = injector.get(LocaleService);
localeService.load({
[LocaleType.EN_US]: {
'sheets-hyper-link': {
message: {
refError: 'Invalid Range',
},
},
} as never,
});
localeService.setLocale(LocaleType.EN_US);
service = injector.get(SheetsHyperLinkParserService);
});
afterEach(() => {
univer.dispose();
});
it('should build hyperlinks for sheet, defined name and range object', () => {
const range: IRange = {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
};
expect(service.buildHyperLink(null, 'sheet-1')).toBe('#gid=sheet-1');
expect(service.buildHyperLink('name-1')).toBe('#rangeid=name-1');
expect(service.buildHyperLink(range, 'sheet-1')).toBe('#gid=sheet-1&range=A1:B2');
});
it('should parse external URLs as url links', () => {
const url = 'https://univer.ai';
expect(service.parseHyperLink(url)).toEqual({
type: SheetHyperLinkType.URL,
name: url,
url,
searchObj: null,
});
});
it('should parse range links from the focused workbook when unitid is absent', () => {
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
const univerInstanceService = univer.__getInjector().get(IUniverInstanceService);
univerInstanceService.focusUnit(workbook.getUnitId());
const url = '#gid=sheet-1&range=A1:B2';
expect(service.parseHyperLink(url)).toEqual({
type: SheetHyperLinkType.RANGE,
name: 'Sheet1!A1:B2',
url,
searchObj: {
gid: 'sheet-1',
range: 'A1:B2',
rangeid: '',
unitid: '',
},
});
});
it('should parse defined name links through the real defined names service', () => {
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
const definedNamesService = univer.__getInjector().get(IDefinedNamesService);
definedNamesService.registerDefinedName(workbook.getUnitId(), {
id: 'name-1',
name: 'Total',
formulaOrRefString: 'Sheet1!A1:B2',
});
const url = `#gid=sheet-1&rangeid=name-1&unitid=${workbook.getUnitId()}`;
expect(service.parseHyperLink(url)).toEqual({
type: SheetHyperLinkType.DEFINE_NAME,
name: 'Sheet1!A1:B2',
url,
searchObj: {
gid: 'sheet-1',
range: '',
rangeid: 'name-1',
unitid: workbook.getUnitId(),
},
});
});
it('should parse defined name links from the focused workbook when unitid is absent', () => {
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
const univerInstanceService = univer.__getInjector().get(IUniverInstanceService);
const definedNamesService = univer.__getInjector().get(IDefinedNamesService);
univerInstanceService.focusUnit(workbook.getUnitId());
definedNamesService.registerDefinedName(workbook.getUnitId(), {
id: 'name-1',
name: 'Total',
formulaOrRefString: 'Sheet1!A1:B2',
});
const url = service.buildHyperLink('name-1');
expect(service.parseHyperLink(url)).toEqual({
type: SheetHyperLinkType.DEFINE_NAME,
name: 'Sheet1!A1:B2',
url,
searchObj: {
gid: '',
range: '',
rangeid: 'name-1',
unitid: '',
},
});
});
it('should parse sheet links by sheet id', () => {
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
const url = `#gid=sheet-1&unitid=${workbook.getUnitId()}`;
expect(service.parseHyperLink(url)).toEqual({
type: SheetHyperLinkType.SHEET,
name: 'Sheet1',
url,
searchObj: {
gid: 'sheet-1',
range: '',
rangeid: '',
unitid: workbook.getUnitId(),
},
});
});
it('should return invalid for missing workbook, invalid range and missing defined names', () => {
univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, createWorkbookData());
expect(service.parseHyperLink('#gid=sheet-1&range=A1&unitid=missing').type).toBe(SheetHyperLinkType.INVALID);
expect(service.parseHyperLink(`#gid=sheet-1&range=${ERROR_RANGE}&unitid=book-1`).type).toBe(SheetHyperLinkType.INVALID);
expect(service.parseHyperLink('#gid=sheet-1&rangeid=missing&unitid=book-1')).toEqual({
type: SheetHyperLinkType.INVALID,
name: 'Invalid Range',
url: '#gid=sheet-1&rangeid=missing&unitid=book-1',
searchObj: {
gid: 'sheet-1',
range: '',
rangeid: 'missing',
unitid: 'book-1',
},
});
});
});

View File

@@ -0,0 +1,146 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IRange, Nullable, Workbook } from '@univerjs/core';
import type { LocaleKey } from '../locale/types';
import { Inject, isValidRange, IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
import {
deserializeRangeWithSheet,
IDefinedNamesService,
serializeRange,
serializeRangeWithSheet,
} from '@univerjs/engine-formula';
import { ERROR_RANGE } from '../types/const';
import { SheetHyperLinkType } from '../types/enums/hyper-link-type';
export interface ISheetUrlParams {
gid?: string;
range?: string;
rangeid?: string;
unitid?: string;
}
export interface ISheetHyperLinkInfo {
type: SheetHyperLinkType;
name: string;
url: string;
searchObj: Nullable<ISheetUrlParams>;
}
export class SheetsHyperLinkParserService {
constructor(
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService,
@Inject(LocaleService) private _localeService: LocaleService,
@IDefinedNamesService private _definedNamesService: IDefinedNamesService
) {}
buildHyperLink(defineNameId: string): string;
buildHyperLink(range: IRange | null, sheetId: string): string;
buildHyperLink(defineNameIdOrRange: string | IRange | null, sheetId?: string): string {
if (typeof defineNameIdOrRange === 'string') {
return `#${SheetHyperLinkType.DEFINE_NAME}=${defineNameIdOrRange}`;
}
let result = `#${SheetHyperLinkType.SHEET}=${sheetId}`;
if (defineNameIdOrRange) {
result += `&${SheetHyperLinkType.RANGE}=${serializeRange(defineNameIdOrRange)}`;
}
return result;
}
parseHyperLink(urlStr: string): ISheetHyperLinkInfo {
if (urlStr.startsWith('#')) {
const search = new URLSearchParams(urlStr.slice(1));
const searchObj: ISheetUrlParams = {
gid: search.get('gid') ?? '',
range: search.get('range') ?? '',
rangeid: search.get('rangeid') ?? '',
unitid: search.get('unitid') ?? '',
};
const urlInfo = this._getURLName(searchObj);
return {
type: urlInfo.type,
name: urlInfo.name,
url: urlStr,
searchObj,
};
} else {
return {
type: SheetHyperLinkType.URL,
name: urlStr,
url: urlStr,
searchObj: null,
};
}
}
private _getURLName(params: ISheetUrlParams) {
const { gid, range, rangeid, unitid } = params;
const workbook = unitid ?
this._univerInstanceService.getUnit<Workbook>(unitid, UniverInstanceType.UNIVER_SHEET)
: this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const invalidLink = {
type: SheetHyperLinkType.INVALID,
name: this._localeService.t<LocaleKey>('sheets-hyper-link.message.refError'),
};
if (!workbook) {
return invalidLink;
}
const sheet = gid ? workbook.getSheetBySheetId(gid) : workbook.getActiveSheet();
const sheetName = sheet?.getName() ?? '';
if (range) {
if (!sheet) return invalidLink;
const rangeObj = deserializeRangeWithSheet(range).range;
if (isValidRange(rangeObj, sheet) && range !== ERROR_RANGE) {
return {
type: SheetHyperLinkType.RANGE,
name: serializeRangeWithSheet(sheetName, rangeObj),
};
}
return invalidLink;
}
if (rangeid) {
const range = this._definedNamesService.getValueById(workbook.getUnitId(), rangeid);
if (range) {
return {
type: SheetHyperLinkType.DEFINE_NAME,
name: range.formulaOrRefString,
};
}
return invalidLink;
}
if (gid) {
const worksheet = workbook.getSheetBySheetId(gid);
if (worksheet) {
return {
type: SheetHyperLinkType.SHEET,
name: worksheet.getName(),
};
}
return invalidLink;
}
return invalidLink;
}
}

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SHEET_HYPER_LINK_PLUGIN = 'SHEET_HYPER_LINK_PLUGIN';
export const ERROR_RANGE = 'err';

View File

@@ -0,0 +1,23 @@
/**
* 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 SheetHyperLinkType {
SHEET = 'gid',
RANGE = 'range',
DEFINE_NAME = 'rangeid',
INVALID = 'invalid',
URL = 'url',
}

View File

@@ -0,0 +1,45 @@
/**
* 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 interface ICellLinkContent {
/**
* #gid=sheet1&range=A1
* sub sheet: #gid=sheet1
* namedRange: #rangeid=123
* http: https://baidu.com
* file: file://a.xlsx
*/
payload: string;
/**
* only for notify, if you wan't to read the display of text, use cell-value of cell-matrix instead
*/
display?: string;
}
export interface ISheetHyperLink extends ICellLinkContent {
/**
* unique id
*/
id: string;
/**
* row of link
*/
row: number;
/**
* col of link
*/
column: number;
}

View File

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

View File

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

View File

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