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,243 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICellData } from '@univerjs/core';
import type { IHoverRichTextInfo } from '@univerjs/sheets-ui';
import type { IHyperLinkPopupOptions } from '../../../services/popup.service';
import {
CustomRangeType,
ICommandService,
Injector,
IPermissionService,
IUniverInstanceService,
RichTextBuilder,
toDisposable,
} from '@univerjs/core';
import { DocSelectionManagerService } from '@univerjs/docs';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SheetPermissionCheckController } from '@univerjs/sheets';
import { HoverManagerService, IEditorBridgeService } from '@univerjs/sheets-ui';
import { of, Subject } from 'rxjs';
import { describe, expect, it } from 'vitest';
import { ISheetsHyperLinkPopupService } from '../../../services/popup.service';
import { SheetsHyperLinkMobilePopupController } from '../popup.controller';
class TestHoverManagerService {
readonly currentClickedCell$ = new Subject<IHoverRichTextInfo>();
}
class TestSheetsHyperLinkPopupService {
readonly shown: IHyperLinkPopupOptions[] = [];
hideCount = 0;
showPopup(location: IHyperLinkPopupOptions): void {
this.shown.push(location);
}
hideCurrentPopup(): void {
this.hideCount += 1;
}
endEditing(): void {}
}
class TestRenderManagerService {
getRenderUnitById() {
return {
with: () => ({
getSkeletonParam: () => ({
skeleton: {
overflowCache: {
forValue: () => {},
},
},
}),
}),
};
}
}
class TestPermissionService {
composePermission() {
return [{ value: true }];
}
}
class TestSheetPermissionCheckController {
permissionCheckWithRanges(): boolean {
return true;
}
}
class TestCommandService {
onCommandExecuted() {
return toDisposable(() => undefined);
}
}
class TestEditorBridgeService {
readonly currentEditCellState$ = of(null);
readonly visible$ = of({ visible: false });
}
class TestDocSelectionManagerService {
readonly textSelection$ = new Subject();
}
class TestUniverInstanceService {
cell: ICellData = {};
readonly worksheet = {
getCell: () => this.cell,
getCellRaw: () => ({}),
getCellStyleOnly: () => ({}),
};
readonly workbook = {
getSheetBySheetId: () => this.worksheet,
getStyles: () => ({
getStyleByCell: () => ({}),
}),
};
getUnit() {
return this.workbook;
}
}
function createMobilePopupControllerTestBed() {
const injector = new Injector();
injector.add([HoverManagerService, { useClass: TestHoverManagerService as never }]);
injector.add([ISheetsHyperLinkPopupService, { useClass: TestSheetsHyperLinkPopupService as never }]);
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
injector.add([IPermissionService, { useClass: TestPermissionService as never }]);
injector.add([SheetPermissionCheckController, { useClass: TestSheetPermissionCheckController as never }]);
injector.add([ICommandService, { useClass: TestCommandService as never }]);
injector.add([IEditorBridgeService, { useClass: TestEditorBridgeService as never }]);
injector.add([DocSelectionManagerService, { useClass: TestDocSelectionManagerService as never }]);
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
injector.add([SheetsHyperLinkMobilePopupController]);
injector.get(SheetsHyperLinkMobilePopupController);
return {
hoverManagerService: injector.get(HoverManagerService) as unknown as TestHoverManagerService,
popupService: injector.get(ISheetsHyperLinkPopupService) as unknown as TestSheetsHyperLinkPopupService,
univerInstanceService: injector.get(IUniverInstanceService) as unknown as TestUniverInstanceService,
};
}
describe('SheetsHyperLinkMobilePopupController', () => {
it('opens the hyperlink popup when the hyperlink text is clicked', () => {
const { hoverManagerService, popupService } = createMobilePopupControllerTestBed();
const customRange = {
rangeId: 'link-1',
rangeType: CustomRangeType.HYPERLINK,
startIndex: 0,
endIndex: 5,
properties: { url: 'https://univer.ai' },
};
hoverManagerService.currentClickedCell$.next({
location: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
},
position: { startX: 0, startY: 0, endX: 20, endY: 20 },
customRange,
rect: { left: 0, top: 0, right: 20, bottom: 20 },
});
expect(popupService.shown).toEqual([
expect.objectContaining({
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
customRange,
editPermission: true,
copyPermission: true,
}),
]);
expect(popupService.hideCount).toBe(0);
});
it('opens the hyperlink popup when the empty area of a hyperlink cell is clicked', () => {
const { hoverManagerService, popupService, univerInstanceService } = createMobilePopupControllerTestBed();
const documentData = RichTextBuilder.create()
.insertLink('Univer', 'https://univer.ai')
.getData();
const customRange = documentData.body!.customRanges![0];
univerInstanceService.cell = {
p: documentData,
};
hoverManagerService.currentClickedCell$.next({
location: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
},
position: { startX: 0, startY: 0, endX: 100, endY: 20 },
});
expect(popupService.shown).toEqual([
expect.objectContaining({
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
customRange,
editPermission: true,
copyPermission: true,
}),
]);
expect(popupService.hideCount).toBe(0);
});
it('shows all hyperlinks when the empty area of a cell containing multiple hyperlinks is clicked', () => {
const { hoverManagerService, popupService, univerInstanceService } = createMobilePopupControllerTestBed();
const documentData = RichTextBuilder.create()
.insertLink('Univer', 'https://univer.ai')
.insertText(' ')
.insertLink('Docs', 'https://docs.univer.ai')
.getData();
const firstCustomRange = documentData.body!.customRanges![0];
univerInstanceService.cell = {
p: documentData,
};
hoverManagerService.currentClickedCell$.next({
location: {
unitId: 'unit-1',
subUnitId: 'sheet-1',
row: 2,
col: 3,
},
position: { startX: 0, startY: 0, endX: 100, endY: 20 },
});
expect(popupService.shown).toEqual([
expect.objectContaining({
customRange: firstCustomRange,
showAll: true,
}),
]);
expect(popupService.hideCount).toBe(0);
});
});

View File

@@ -0,0 +1,34 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, Inject } from '@univerjs/core';
import { LinkIcon } from '@univerjs/icons';
import { ComponentManager, IconManager } from '@univerjs/ui';
import { MobileCellLinkEdit } from '../../views/MobileCellLinkEdit';
import { MobileCellLinkPopup } from '../../views/MobileCellLinkPopup';
export class MobileComponentsController extends Disposable {
constructor(
@Inject(ComponentManager) componentManager: ComponentManager,
@Inject(IconManager) iconManager: IconManager
) {
super();
this.disposeWithMe(componentManager.register(MobileCellLinkPopup.componentKey, MobileCellLinkPopup));
this.disposeWithMe(componentManager.register(MobileCellLinkEdit.componentKey, MobileCellLinkEdit));
this.disposeWithMe(iconManager.register({ LinkIcon }));
}
}

View File

@@ -0,0 +1,298 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICustomRange, Nullable, Workbook, Worksheet } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { Subscription } from 'rxjs';
import {
CustomRangeType,
Disposable,
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
ICommandService,
Inject,
IPermissionService,
IUniverInstanceService,
Rectangle,
UniverInstanceType,
} from '@univerjs/core';
import { DocSelectionManagerService } from '@univerjs/docs';
import { DocEventManagerService } from '@univerjs/docs-ui';
import { IRenderManagerService } from '@univerjs/engine-render';
import {
ClearSelectionAllCommand,
ClearSelectionContentCommand,
ClearSelectionFormatCommand,
RangeProtectionPermissionEditPoint,
RangeProtectionPermissionViewPoint,
SheetPermissionCheckController,
WorkbookCopyPermission,
WorkbookEditablePermission,
WorkbookViewPermission,
WorksheetCopyPermission,
WorksheetEditPermission,
WorksheetInsertHyperlinkPermission,
WorksheetViewPermission,
} from '@univerjs/sheets';
import { HoverManagerService, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { map, switchMap } from 'rxjs';
import { ISheetsHyperLinkPopupService } from '../../services/popup.service';
import { HyperLinkEditSourceType } from '../../types/enums/edit-source';
export class SheetsHyperLinkMobilePopupController extends Disposable {
constructor(
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
@Inject(ISheetsHyperLinkPopupService) private readonly _sheetsHyperLinkPopupService: ISheetsHyperLinkPopupService,
@Inject(IRenderManagerService) private readonly _renderManagerService: IRenderManagerService,
@Inject(IPermissionService) private readonly _permissionService: IPermissionService,
@Inject(SheetPermissionCheckController) private readonly _sheetPermissionCheckController: SheetPermissionCheckController,
@ICommandService private readonly _commandService: ICommandService,
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._initClickListener();
this._initCommandListener();
this._initClickEditingListener();
this._initTextSelectionListener();
}
private _getLinkPermission(location: ISheetLocationBase) {
const { unitId, subUnitId, row, col } = location;
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getSheetBySheetId(subUnitId);
if (!worksheet) {
return {
viewPermission: false,
editPermission: false,
copyPermission: false,
};
}
const range = [{ startRow: row, startColumn: col, endRow: row, endColumn: col }];
const viewPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
workbookTypes: [WorkbookViewPermission],
worksheetTypes: [WorksheetViewPermission],
rangeTypes: [RangeProtectionPermissionViewPoint],
}, range, unitId, subUnitId);
let editPermission = this._sheetPermissionCheckController.permissionCheckWithRanges({
workbookTypes: [WorkbookEditablePermission],
worksheetTypes: [WorksheetEditPermission, WorksheetInsertHyperlinkPermission],
rangeTypes: [RangeProtectionPermissionEditPoint],
}, range, unitId, subUnitId);
const cell = worksheet.getCellRaw(row, col);
if (cell?.f?.startsWith('=HYPERLINK(')) {
editPermission = false;
}
const copyPermission = this._permissionService.composePermission([
new WorkbookCopyPermission(unitId).id,
new WorksheetCopyPermission(unitId, subUnitId).id,
]).every((permission) => permission.value);
return {
viewPermission,
editPermission,
copyPermission,
};
}
private _getCellHyperLinkInfo(
worksheet: Worksheet,
row: number,
col: number,
clickedCustomRange?: Nullable<ICustomRange>
) {
const customRanges = worksheet.getCell(row, col)?.p?.body?.customRanges ?? [];
const cellHyperLinks = customRanges.filter(
(customRange) => customRange.rangeType === CustomRangeType.HYPERLINK
);
const clickedHyperLink = clickedCustomRange?.rangeType === CustomRangeType.HYPERLINK
? clickedCustomRange
: null;
return {
cellHyperLinks,
clickedHyperLink,
customRange: clickedHyperLink ?? cellHyperLinks[0],
};
}
private _initClickListener(): void {
this.disposeWithMe(this._hoverManagerService.currentClickedCell$.subscribe((currentCell) => {
if (!currentCell) {
this._sheetsHyperLinkPopupService.hideCurrentPopup();
return;
}
const { unitId, subUnitId, row, col } = currentCell.location;
const renderer = this._renderManagerService.getRenderUnitById(unitId);
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
const worksheet = workbook?.getSheetBySheetId(subUnitId);
if (!renderer || !workbook || !worksheet) {
return;
}
const skeleton = renderer.with(SheetSkeletonManagerService)
.getSkeletonParam(subUnitId)
?.skeleton;
let targetRow = row;
let targetCol = col;
if (skeleton) {
skeleton.overflowCache.forValue((overflowRow, overflowCol, value) => {
if (Rectangle.contains(value, {
startColumn: col,
endColumn: col,
startRow: row,
endRow: row,
})) {
targetRow = overflowRow;
targetCol = overflowCol;
}
});
}
const { cellHyperLinks, clickedHyperLink, customRange } = this._getCellHyperLinkInfo(
worksheet,
targetRow,
targetCol,
currentCell.customRange
);
if (!customRange) {
this._sheetsHyperLinkPopupService.hideCurrentPopup();
return;
}
const location = { unitId, subUnitId, row: targetRow, col: targetCol };
const { viewPermission, editPermission, copyPermission } = this._getLinkPermission(location);
if (!viewPermission) {
this._sheetsHyperLinkPopupService.hideCurrentPopup();
return;
}
const cellStyle = worksheet.getCellStyleOnly(targetRow, targetCol);
const style = workbook.getStyles().getStyleByCell(cellStyle);
const showAll = Boolean(style?.tr?.a) || (!clickedHyperLink && cellHyperLinks.length > 1);
this._sheetsHyperLinkPopupService.showPopup({
...location,
editPermission,
copyPermission,
customRange,
customRangeRect: currentCell.rect,
type: HyperLinkEditSourceType.VIEWING,
showAll,
});
}));
}
private _initClickEditingListener(): void {
let subscription: Nullable<Subscription> = null;
this.disposeWithMe(
this._editorBridgeService.currentEditCellState$
.pipe(switchMap((state) => this._editorBridgeService.visible$.pipe(map((visible) => ({ visible, state })))))
.subscribe(({ visible, state }) => {
if (!state || state.editorUnitId !== DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
return;
}
if (!visible.visible) {
subscription?.unsubscribe();
this._sheetsHyperLinkPopupService.hideCurrentPopup(HyperLinkEditSourceType.EDITING);
this._sheetsHyperLinkPopupService.endEditing(HyperLinkEditSourceType.EDITING);
return;
}
const { editorUnitId, unitId, sheetId, row, column } = state;
const renderer = this._renderManagerService.getRenderUnitById(editorUnitId);
if (!renderer) {
return;
}
const { editPermission, viewPermission, copyPermission } = this._getLinkPermission({
unitId,
subUnitId: sheetId,
row,
col: column,
});
if (!viewPermission) {
return;
}
const docEventService = renderer.with(DocEventManagerService);
subscription?.unsubscribe();
subscription = docEventService.clickCustomRanges$.subscribe((customRange) => {
if (customRange.range.rangeType !== CustomRangeType.HYPERLINK) {
this._sheetsHyperLinkPopupService.hideCurrentPopup();
return;
}
const rect = customRange.rects[customRange.rects.length - 1];
const skeleton = this._renderManagerService.getRenderUnitById(unitId)
?.with(SheetSkeletonManagerService)
.getSkeletonParam(sheetId)
?.skeleton;
if (!skeleton || !rect) {
return;
}
const canvasClientRect = renderer.engine.getCanvasElement().getBoundingClientRect();
this._sheetsHyperLinkPopupService.showPopup({
unitId,
subUnitId: sheetId,
row,
col: column,
customRange: customRange.range,
customRangeRect: {
left: rect.left + canvasClientRect.left,
top: rect.top + canvasClientRect.top,
bottom: rect.bottom + canvasClientRect.top,
right: rect.right + canvasClientRect.left,
},
editPermission,
copyPermission,
type: HyperLinkEditSourceType.EDITING,
});
});
})
);
this.disposeWithMe(() => subscription?.unsubscribe());
}
private _initTextSelectionListener(): void {
this.disposeWithMe(this._textSelectionManagerService.textSelection$.subscribe((selection) => {
if (selection?.unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
this._sheetsHyperLinkPopupService.endEditing(HyperLinkEditSourceType.EDITING);
}
}));
}
private _initCommandListener(): void {
const hideCommandList = [
ClearSelectionContentCommand.id,
ClearSelectionAllCommand.id,
ClearSelectionFormatCommand.id,
];
this.disposeWithMe(this._commandService.onCommandExecuted((command) => {
if (hideCommandList.includes(command.id)) {
this._sheetsHyperLinkPopupService.hideCurrentPopup();
}
}));
}
}