feat(sheets-note): support attach note to cells (#5125)

This commit is contained in:
ZhangWei
2025-04-30 16:45:46 +08:00
committed by GitHub
parent 1abce4b37a
commit 1a58d80e5d
96 changed files with 4383 additions and 139 deletions
+1 -1
View File
@@ -45,7 +45,7 @@
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/icons-svg": "^0.3.22",
"@univerjs/protocol": "0.1.45"
}
+3
View File
@@ -27,6 +27,7 @@
"@univerjs/docs-drawing-ui": "workspace:*",
"@univerjs/docs-hyper-link-ui": "workspace:*",
"@univerjs/docs-mention-ui": "workspace:*",
"@univerjs/docs-quick-insert-ui": "workspace:*",
"@univerjs/docs-thread-comment-ui": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
@@ -55,6 +56,8 @@
"@univerjs/sheets-formula-ui": "workspace:*",
"@univerjs/sheets-hyper-link": "workspace:*",
"@univerjs/sheets-hyper-link-ui": "workspace:*",
"@univerjs/sheets-note": "workspace:*",
"@univerjs/sheets-note-ui": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
"@univerjs/sheets-numfmt-ui": "workspace:*",
"@univerjs/sheets-sort": "workspace:*",
+6 -3
View File
@@ -31,6 +31,8 @@ import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validati
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
import { SheetsNotePlugin } from '@univerjs/sheets-note';
import { SheetsNoteUIPlugin } from '@univerjs/sheets-note-ui';
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort';
import { UniverSheetsBindingSourcePlugin } from '@univerjs/sheets-source-binding';
@@ -38,13 +40,13 @@ import { UniverSheetTablePlugin } from '@univerjs/sheets-table';
import { UniverSheetsThreadCommentPlugin } from '@univerjs/sheets-thread-comment';
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor';
import { UniverUIPlugin } from '@univerjs/ui';
import { enUS, faIR, frFR, ruRU, viVN, zhCN, zhTW } from '../locales';
import { UniverSheetsCustomMenuPlugin } from './custom-menu';
import ImportCSVButtonPlugin from './custom-plugin/import-csv-button';
import ImportCSVButtonPlugin from './custom-plugin/import-csv-button';
import '@univerjs/sheets/facade';
import '@univerjs/ui/facade';
import '@univerjs/docs-ui/facade';
@@ -131,7 +133,8 @@ function createNewInstance() {
univer.registerPlugin(UniverSheetTablePlugin);
univer.registerPlugin(UniverNetworkPlugin);
univer.registerPlugin(SheetsNotePlugin);
univer.registerPlugin(SheetsNoteUIPlugin);
univer.registerPlugin(ImportCSVButtonPlugin);
// If we are running in e2e platform, we should immediately register the debugger plugin.
@@ -24108,6 +24108,10 @@ export const DEFAULT_WORKBOOK_DATA_DEMO: IWorkbookData = {
'dv-test': dv2,
}),
},
{
name: 'SHEET_NOTE_PLUGIN',
data: '{"sheet-0011":{"8":{"7":{"width":160,"height":60,"note":"overlap"}},"10":{"9":{"width":160,"height":60,"note":"This is a note of sheet","show":true}},"26":{"9":{"width":160,"height":60,"note":"This is note 2"}}}}',
},
{
name: 'SHEET_FILTER_PLUGIN',
data: JSON.stringify({
@@ -68,7 +68,7 @@
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-filter": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
+1 -1
View File
@@ -68,7 +68,7 @@
"@univerjs/docs-drawing-ui": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/mockdata": "workspace:*",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-drawing-ui": "workspace:*",
@@ -70,7 +70,7 @@
"@univerjs/docs": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/rpc": "workspace:*",
"@univerjs/sheets-formula": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
@@ -64,7 +64,7 @@
"@univerjs/design": "workspace:*",
"@univerjs/docs": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-drawing-ui": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
@@ -62,7 +62,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/slides-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"@univerjs/uniui": "workspace:*"
+1 -1
View File
@@ -65,7 +65,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
+1 -1
View File
@@ -75,7 +75,7 @@
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tooltip": "^1.2.4",
"@rc-component/trigger": "^2.2.6",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
+1 -1
View File
@@ -74,7 +74,7 @@
"@univerjs/drawing": "workspace:*",
"@univerjs/drawing-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
+1 -1
View File
@@ -72,7 +72,7 @@
"@univerjs/docs-hyper-link": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
+1 -1
View File
@@ -75,7 +75,7 @@
"@univerjs/drawing": "workspace:*",
"@univerjs/drawing-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
+1 -1
View File
@@ -64,7 +64,7 @@
"@univerjs/docs": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/thread-comment-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
+1 -1
View File
@@ -84,7 +84,7 @@
"@univerjs/docs": "workspace:*",
"@univerjs/drawing": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
+1 -1
View File
@@ -70,7 +70,7 @@
"@univerjs/design": "workspace:*",
"@univerjs/drawing": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
+1 -1
View File
@@ -71,7 +71,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/ui": "workspace:*"
},
"devDependencies": {
@@ -73,7 +73,7 @@
"@univerjs/design": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-conditional-formatting": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
@@ -80,7 +80,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
@@ -75,7 +75,7 @@
"@univerjs/design": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-data-validation": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
+1 -1
View File
@@ -84,7 +84,7 @@
"@univerjs/drawing": "workspace:*",
"@univerjs/drawing-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-drawing": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
+1 -1
View File
@@ -69,7 +69,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/rpc": "workspace:*",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-filter": "workspace:*",
+1 -1
View File
@@ -84,7 +84,7 @@
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-formula": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
+1 -1
View File
@@ -85,7 +85,7 @@
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-data-validation": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
+37
View File
@@ -0,0 +1,37 @@
# @univerjs/sheets-note-ui
| Package | Description | Version | License | Downloads | Continuous Integration | Coverage |
|---------|-------------|---------|----------|-----------|----------------------|----------|
| `@univerjs/sheets-note-ui` | `UniverSheetsNote` | [![][npm-version-shield]][npm-version-link] | ![][npm-license-shield] | ![][npm-downloads-shield] | ⭕️ | ⭕️ |
## Introduction
`@univerjs/sheets-note-ui` provides the note/annotation function of Univer Sheets.
## Usage
### Installation
```shell
npm install @univerjs/sheets-note-ui
```
or
```shell
pnpm add @univerjs/sheets-note-ui
```
### Example
```ts
import { UniverSheetsNotePlugin, INoteMentionDataService} from '@univerjs/sheets-note-ui';
// Register the plugin
univer.registerPlugin(UniverSheetsNotePlugin);
```
[npm-version-shield]: https://img.shields.io/npm/v/@univerjs/sheets-note-ui?style=flat-square
[npm-version-link]: https://npmjs.com/package/@univerjs/sheets-note-ui
[npm-license-shield]: https://img.shields.io/npm/l/@univerjs/sheets-note-ui?style=flat-square
[npm-downloads-shield]: https://img.shields.io/npm/dm/@univerjs/sheets-note-ui?style=flat-square
+83
View File
@@ -0,0 +1,83 @@
{
"name": "@univerjs/sheets-note-ui",
"version": "0.6.10",
"private": false,
"description": "Univer sheets note UI plugin",
"author": "DreamNum <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": [],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./locale/*": "./src/locale/*.ts",
"./facade": "./src/facade/index.ts"
},
"main": "./lib/index.js",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"files": [
"lib"
],
"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"
},
"./lib/*": "./lib/*"
}
},
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"build": "univer-cli build"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-note": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"less": "^4.1.3",
"typescript": "^5.4.3",
"vite": "^4.5.2",
"vitest": "^1.3.1"
}
}
@@ -0,0 +1 @@
export { default } from '@univerjs-infra/shared/postcss';
@@ -0,0 +1,58 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IAccessor, ICommand, Workbook } from '@univerjs/core';
import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetsNotePopupService } from '../../services/sheets-note-popup.service';
export interface IAddNotePopupOperationParams {
trigger?: string;
}
export const AddNotePopupOperation: ICommand = {
id: 'sheet.operation.add-note-popup',
type: CommandType.OPERATION,
handler: async (accessor: IAccessor, params?: IAddNotePopupOperationParams) => {
const selectionService = accessor.get(SheetsSelectionsService);
const notePopupService = accessor.get(SheetsNotePopupService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return false;
}
const worksheet = workbook.getActiveSheet();
const lastSelection = selectionService.getCurrentLastSelection();
if (!lastSelection?.primary) {
return false;
}
const { primary } = lastSelection;
notePopupService.showPopup({
unitId: workbook.getUnitId(),
subUnitId: worksheet.getSheetId(),
row: primary.actualRow,
col: primary.actualColumn,
temp: false,
trigger: params?.trigger,
});
return true;
},
};
@@ -0,0 +1,41 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { MenuSchemaType } from '@univerjs/ui';
import { SheetDeleteNoteCommand, SheetToggleNotePopupCommand } from '@univerjs/sheets-note';
import { ContextMenuGroup, ContextMenuPosition } from '@univerjs/ui';
import { AddNotePopupOperation } from '../commands/operations/add-note-popup.operation';
import { sheetDeleteNoteMenuFactory, sheetNoteContextMenuFactory, sheetNoteToggleMenuFactory } from './note.menu';
export const menuSchema: MenuSchemaType = {
[ContextMenuPosition.MAIN_AREA]: {
[ContextMenuGroup.OTHERS]: {
order: 0,
[AddNotePopupOperation.id]: {
order: 0,
menuItemFactory: sheetNoteContextMenuFactory,
},
[SheetDeleteNoteCommand.id]: {
order: 0,
menuItemFactory: sheetDeleteNoteMenuFactory,
},
[SheetToggleNotePopupCommand.id]: {
order: 0,
menuItemFactory: sheetNoteToggleMenuFactory,
},
},
},
};
@@ -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 { IAccessor } from '@univerjs/core';
import type { IMenuItem } from '@univerjs/ui';
import type { Observable } from 'rxjs';
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService, WorkbookEditablePermission, WorksheetEditPermission } from '@univerjs/sheets';
import { SheetDeleteNoteCommand, SheetsNoteModel, SheetToggleNotePopupCommand } from '@univerjs/sheets-note';
import { getCurrentRangeDisable$ } from '@univerjs/sheets-ui';
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
import { combineLatest, map } from 'rxjs';
import { AddNotePopupOperation } from '../commands/operations/add-note-popup.operation';
export const SHEET_NOTE_CONTEXT_MENU_ID = 'sheet.menu.note';
function getHasNote$(accessor: IAccessor): Observable<boolean> {
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
const univerInstanceService = accessor.get(IUniverInstanceService);
return sheetsSelectionsService.selectionMoveEnd$.pipe(map(() => {
const selection = sheetsSelectionsService.getCurrentLastSelection();
if (!selection?.primary) return false;
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const { actualColumn, actualRow } = selection.primary;
const noteModel = accessor.get(SheetsNoteModel);
return Boolean(noteModel.getNote(target.unitId, target.subUnitId, actualRow, actualColumn));
}));
}
export function sheetNoteContextMenuFactory(accessor: IAccessor): IMenuItem {
return {
id: AddNotePopupOperation.id,
type: MenuItemType.BUTTON,
title: 'rightClick.addNote',
icon: 'AddNoteSingle',
hidden$: combineLatest([getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET), getHasNote$(accessor)])
.pipe(map(([hidden, hasNote]) => hidden || hasNote)),
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetEditPermission] }),
commandId: AddNotePopupOperation.id,
};
}
export function sheetDeleteNoteMenuFactory(accessor: IAccessor): IMenuItem {
return {
id: SheetDeleteNoteCommand.id,
type: MenuItemType.BUTTON,
title: 'rightClick.deleteNote',
icon: 'DeleteNoteSingle',
hidden$: getHasNote$(accessor).pipe(map((hasNote) => !hasNote)),
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetEditPermission] }),
};
}
export function sheetNoteToggleMenuFactory(accessor: IAccessor): IMenuItem {
return {
id: SheetToggleNotePopupCommand.id,
type: MenuItemType.BUTTON,
title: 'rightClick.toggleNote',
icon: 'HideNoteSingle',
hidden$: getHasNote$(accessor).pipe(map((hasNote) => !hasNote)),
};
}
@@ -0,0 +1,79 @@
/**
* 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 { Disposable, Inject, InterceptorEffectEnum, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
import { SheetsNoteModel } from '@univerjs/sheets-note';
import { debounceTime } from 'rxjs';
export class SheetsCellContentController extends Disposable {
constructor(
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
this._initViewModelIntercept();
this._initSkeletonChange();
}
private _initViewModelIntercept() {
this.disposeWithMe(
this._sheetInterceptorService.intercept(
INTERCEPTOR_POINT.CELL_CONTENT,
{
effect: InterceptorEffectEnum.Style,
handler: (cell, pos, next) => {
const { row, col, unitId, subUnitId } = pos;
const note = this._sheetsNoteModel.getNote(unitId, subUnitId, row, col);
if (note) {
return next({
...cell,
markers: {
...cell?.markers,
tr: {
color: '#FFBD37',
size: 6,
},
},
});
}
return next(cell);
},
priority: 100,
}
)
);
}
private _initSkeletonChange() {
const markSkeletonDirty = () => {
const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) return;
const unitId = workbook.getUnitId();
const currentRender = this._renderManagerService.getRenderById(unitId);
currentRender?.mainComponent?.makeForceDirty();
};
this.disposeWithMe(this._sheetsNoteModel.change$.pipe(debounceTime(16)).subscribe(() => {
markSkeletonDirty();
}));
}
}
@@ -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 { IDisposable, Nullable, Workbook } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { ISheetNote } from '@univerjs/sheets-note';
import { Disposable, Inject, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import { SheetsNoteModel } from '@univerjs/sheets-note';
import { CellPopupManagerService } from '@univerjs/sheets-ui';
import { of, switchMap } from 'rxjs';
import { SheetsNotePopupService } from '../services/sheets-note-popup.service';
import { SHEET_NOTE_COMPONENT } from '../views/config';
export class SheetsNoteAttachmentController extends Disposable {
private _noteMatrix = new ObjectMatrix<IDisposable>();
constructor(
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel,
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
@Inject(CellPopupManagerService) private readonly _cellPopupManagerService: CellPopupManagerService,
@Inject(SheetsNotePopupService) private readonly _sheetsNotePopupService: SheetsNotePopupService
) {
super();
this._initNoteChangeListener();
}
private _showPopup(unitId: string, sheetId: string, row: number, col: number) {
this._sheetsNotePopupService.hidePopup(true);
return this._cellPopupManagerService.showPopup(
{
unitId,
subUnitId: sheetId,
row,
col,
},
{
componentKey: SHEET_NOTE_COMPONENT,
direction: 'horizontal',
extraProps: {
location: {
unitId,
subUnitId: sheetId,
row,
col,
} as ISheetLocationBase,
},
priority: 3,
}
);
}
private _initSheet(targetUnitId: string, targetSheetId: string) {
const oldMatrix = this._noteMatrix;
oldMatrix.forValue((_, __, disposable) => {
disposable.dispose();
});
this._noteMatrix = new ObjectMatrix();
const handleNote = (unitId: string, sheetId: string, row: number, col: number, note: Nullable<ISheetNote>) => {
const matrix = this._noteMatrix;
const disposable = matrix.getValue(row, col);
if (note?.show) {
if (!disposable) {
const newDisposable = this._showPopup(unitId, sheetId, row, col);
if (newDisposable) {
matrix.setValue(row, col, newDisposable);
}
}
} else {
if (disposable) {
disposable.dispose();
matrix.realDeleteValue(row, col);
}
}
};
this._sheetsNoteModel.getSheetNotes(targetUnitId, targetSheetId)?.forValue((row, col, note) => {
handleNote(targetUnitId, targetSheetId, row, col, note);
});
return this._sheetsNoteModel.change$.subscribe((change) => {
if (change.unitId !== targetUnitId || change.sheetId !== targetSheetId) {
return;
}
switch (change.type) {
case 'ref': {
const { unitId, sheetId, row, col, newPosition, note } = change;
const matrix = this._noteMatrix;
if (!note.show) return;
const disposable = matrix.getValue(row, col);
if (disposable) {
disposable.dispose();
matrix.realDeleteValue(row, col);
}
const newDisposable = this._showPopup(unitId, sheetId, newPosition.row, newPosition.col);
if (newDisposable) {
matrix.setValue(newPosition.row, newPosition.col, newDisposable);
}
break;
}
case 'update': {
const { unitId, sheetId, row, col, note } = change;
handleNote(unitId, sheetId, row, col, note);
break;
}
default:
break;
}
});
}
private _initNoteChangeListener() {
this.disposeWithMe(
this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET).pipe(
switchMap((workbook) => workbook?.activeSheet$ ?? of(null))
).subscribe((sheet) => {
if (sheet) {
const disposable = this._initSheet(sheet.getUnitId(), sheet.getSheetId());
return () => {
disposable.unsubscribe();
};
} else {
this._noteMatrix.forValue((_, __, disposable) => {
disposable.dispose();
});
this._noteMatrix = new ObjectMatrix();
}
})
);
}
}
@@ -0,0 +1,130 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Nullable } from '@univerjs/core';
import type { ISelectionWithStyle } from '@univerjs/sheets';
import type { IHoverCellPosition } from '@univerjs/sheets-ui';
import { Disposable, Inject, RANGE_TYPE, Rectangle } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { SheetsNoteModel } from '@univerjs/sheets-note';
import { HoverManagerService, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { debounceTime } from 'rxjs/operators';
import { SheetsNotePopupService } from '../services/sheets-note-popup.service';
export class SheetsNotePopupController extends Disposable {
private _isSwitchingSheet = false;
constructor(
@Inject(SheetsNotePopupService) private readonly _sheetsNotePopupService: SheetsNotePopupService,
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel,
@Inject(SheetsSelectionsService) private readonly _sheetSelectionService: SheetsSelectionsService,
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService
) {
super();
this._initSelectionUpdateListener();
this._initEditorBridge();
this._initHoverEvent();
}
private _handleSelectionChange(selections: ISelectionWithStyle[], unitId: string, subUnitId: string) {
const range = selections[0]?.range;
const render = this._renderManagerService.getRenderById(unitId);
const skeleton = render?.with(SheetSkeletonManagerService).getSkeletonParam(subUnitId)?.skeleton;
if (!skeleton) {
return;
}
if (!range) {
return;
}
const actualCell = skeleton.getCellWithCoordByIndex(range.startRow, range.startColumn);
const rangeType = range.rangeType ?? RANGE_TYPE.NORMAL;
if ((rangeType !== RANGE_TYPE.NORMAL || range.endColumn - range.startColumn > 0 || range.endRow - range.startRow > 0) && !((actualCell.isMerged || actualCell.isMergedMainCell) && Rectangle.equals(actualCell.mergeInfo, range))) {
this._sheetsNotePopupService.hidePopup();
return;
}
const row = actualCell.actualRow;
const col = actualCell.actualColumn;
const note = this._sheetsNoteModel.getNote(unitId, subUnitId, row, col);
if (note?.show) return;
if (note) {
this._sheetsNotePopupService.showPopup({
unitId,
subUnitId,
row,
col,
});
} else {
this._sheetsNotePopupService.hidePopup(true);
}
}
private _initSelectionUpdateListener() {
this.disposeWithMe(
this._sheetSelectionService.selectionMoveEnd$.subscribe((selections) => {
if (this._isSwitchingSheet) {
return;
}
const current = this._sheetSelectionService.currentSelectionParam;
if (!current) {
return;
}
this._handleSelectionChange(selections, current.unitId, current.sheetId);
})
);
}
private _initEditorBridge() {
this.disposeWithMe(
this._editorBridgeService.visible$.subscribe((visible) => {
if (visible.visible) {
this._sheetsNotePopupService.hidePopup(true);
}
})
);
}
private _initHoverEvent() {
this.disposeWithMe(
this._hoverManagerService.currentCell$.pipe(debounceTime(100)).subscribe((cell: Nullable<IHoverCellPosition>) => {
if (!cell?.location) return;
const { unitId, subUnitId, row, col } = cell.location;
const note = this._sheetsNoteModel.getNote(unitId, subUnitId, row, col);
if (note?.show) return;
if (note) {
this._sheetsNotePopupService.showPopup({
unitId,
subUnitId,
row,
col,
temp: true,
});
} else {
this._sheetsNotePopupService.hidePopup();
}
})
);
}
}
@@ -0,0 +1,52 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { AddNoteSingle, DeleteNoteSingle, HideNoteSingle } from '@univerjs/icons';
import { ComponentManager, IMenuManagerService } from '@univerjs/ui';
import { AddNotePopupOperation } from '../commands/operations/add-note-popup.operation';
import { SHEET_NOTE_COMPONENT } from '../views/config';
import { SheetsNote } from '../views/note';
import { menuSchema } from './menu.schema';
export class SheetsNoteUIController extends Disposable {
constructor(
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
@Inject(IMenuManagerService) private readonly _menuManagerService: IMenuManagerService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initComponents();
this._initMenu();
this._initCommands();
}
private _initComponents() {
this.disposeWithMe(this._componentManager.register(SHEET_NOTE_COMPONENT, SheetsNote));
this.disposeWithMe(this._componentManager.register('AddNoteSingle', AddNoteSingle));
this.disposeWithMe(this._componentManager.register('DeleteNoteSingle', DeleteNoteSingle));
this.disposeWithMe(this._componentManager.register('HideNoteSingle', HideNoteSingle));
}
private _initMenu() {
this._menuManagerService.mergeMenu(menuSchema);
}
private _initCommands() {
this._commandService.registerCommand(AddNotePopupOperation);
}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './plugin';
export * from './services/sheets-note-popup.service';
export * from './views/note';
export * from './controllers/sheets-note-popup.controller';
export * from './controllers/sheets-cell-content.controller';
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type zhCN from './zh-CN';
const locale: typeof zhCN = {
rightClick: {
addNote: 'Add Note',
deleteNote: 'Delete Note',
toggleNote: 'Show/Hide Note',
},
note: {
placeholder: 'Type here',
},
};
export default locale;
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type zhCN from './zh-CN';
const locale: typeof zhCN = {
rightClick: {
addNote: 'افزودن یادداشت',
deleteNote: 'حذف یادداشت',
toggleNote: 'نمایش/پنهان کردن یادداشت',
},
note: {
placeholder: 'اینجا تایپ کنید',
},
};
export default locale;
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type zhCN from './zh-CN';
const locale: typeof zhCN = {
rightClick: {
addNote: 'Ajouter une note',
deleteNote: 'Supprimer la note',
toggleNote: 'Afficher/Masquer la note',
},
note: {
placeholder: 'Écrivez ici',
},
};
export default locale;
@@ -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 { default as enUS } from './en-US';
export { default as faIR } from './fa-IR';
export { default as frFR } from './fr-FR';
export { default as ruRU } from './ru-RU';
export { default as viVN } from './vi-VN';
export { default as zhCN } from './zh-CN';
export { default as zhTW } from './zh-TW';
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type zhCN from './zh-CN';
const locale: typeof zhCN = {
rightClick: {
addNote: 'Добавить примечание',
deleteNote: 'Удалить примечание',
toggleNote: 'Показать/Скрыть примечание',
},
note: {
placeholder: 'Введите текст здесь',
},
};
export default locale;
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type zhCN from './zh-CN';
const locale: typeof zhCN = {
rightClick: {
addNote: 'Thêm ghi chú',
deleteNote: 'Xóa ghi chú',
toggleNote: 'Hiện/Ẩn ghi chú',
},
note: {
placeholder: 'Nhập tại đây',
},
};
export default locale;
@@ -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.
*/
const locale = {
rightClick: {
addNote: '添加批注',
deleteNote: '删除批注',
toggleNote: '显示/隐藏批注',
},
note: {
placeholder: '在此输入',
},
};
export default locale;
@@ -0,0 +1,30 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type zhCN from './zh-CN';
const locale: typeof zhCN = {
rightClick: {
addNote: '添加批註',
deleteNote: '刪除批註',
toggleNote: '顯示/隱藏批註',
},
note: {
placeholder: '在此輸入',
},
};
export default locale;
+92
View File
@@ -0,0 +1,92 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, touchDependencies, UniverInstanceType } from '@univerjs/core';
import { SheetsNotePlugin } from '@univerjs/sheets-note';
import { menuSchema } from './controllers/menu.schema';
import { SheetsCellContentController } from './controllers/sheets-cell-content.controller';
import { SheetsNoteAttachmentController } from './controllers/sheets-note-attachment.controller';
import { SheetsNotePopupController } from './controllers/sheets-note-popup.controller';
import { SheetsNoteUIController } from './controllers/sheets-note-ui.controller';
import { SheetsNotePopupService } from './services/sheets-note-popup.service';
export const PLUGIN_NAME = 'SHEET_NOTE_UI_PLUGIN';
export const SHEETS_NOTE_UI_PLUGIN_CONFIG_KEY = 'sheets-note-ui';
export interface IUniverSheetsNoteUIPluginConfig {
menu?: {
[key: string]: unknown;
};
defaultNoteSize?: {
width: number;
height: number;
};
}
const defaultPluginConfig: IUniverSheetsNoteUIPluginConfig = {
menu: menuSchema,
};
@DependentOn(SheetsNotePlugin)
export class SheetsNoteUIPlugin extends Plugin {
static override pluginName = PLUGIN_NAME;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
private readonly _config: Partial<IUniverSheetsNoteUIPluginConfig> = defaultPluginConfig,
@Inject(Injector) protected override readonly _injector: Injector,
@IConfigService private readonly _configService: IConfigService
) {
super();
// Manage the plugin configuration.
const { menu, ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
if (menu) {
this._configService.setConfig('menu', menu, { merge: true });
}
this._configService.setConfig(SHEETS_NOTE_UI_PLUGIN_CONFIG_KEY, rest);
}
override onStarting(): void {
([
[SheetsNotePopupService],
[SheetsCellContentController],
[SheetsNotePopupController],
[SheetsNoteUIController],
[SheetsNoteAttachmentController],
] as Dependency[]).forEach((dependency) => {
this._injector.add(dependency);
});
}
override onReady(): void {
touchDependencies(this._injector, [
[SheetsNoteUIController],
[SheetsCellContentController],
]);
}
override onRendered(): void {
touchDependencies(this._injector, [
[SheetsNotePopupController],
[SheetsNoteAttachmentController],
]);
}
}
@@ -0,0 +1,145 @@
/**
* 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, Nullable } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { Disposable, DisposableCollection, Inject } from '@univerjs/core';
import { CellPopupManagerService } from '@univerjs/sheets-ui';
import { IZenZoneService } from '@univerjs/ui';
import { BehaviorSubject } from 'rxjs';
import { SHEET_NOTE_COMPONENT } from '../views/config';
export interface INotePopup extends ISheetLocationBase {
noteId?: string;
temp?: boolean;
trigger?: string;
}
export class SheetsNotePopupService extends Disposable {
private _lastPopup: Nullable<IDisposable> = null;
private _activePopup: Nullable<INotePopup>;
private _activePopup$ = new BehaviorSubject<Nullable<INotePopup>>(null);
activePopup$ = this._activePopup$.asObservable();
get activePopup() {
return this._activePopup;
}
constructor(
@IZenZoneService private readonly _zenZoneService: IZenZoneService,
@Inject(CellPopupManagerService) private readonly _cellPopupManagerService: CellPopupManagerService
) {
super();
this._initZenVisible();
this.disposeWithMe(() => {
this._activePopup$.complete();
});
}
private _initZenVisible() {
this.disposeWithMe(this._zenZoneService.visible$.subscribe((visible) => {
if (visible) {
this.hidePopup();
}
}));
}
showPopup(location: INotePopup, onHide?: () => void) {
const { row, col, unitId, subUnitId } = location;
if (
this._activePopup &&
row === this._activePopup.row &&
col === this._activePopup.col &&
unitId === this._activePopup.unitId &&
subUnitId === this.activePopup?.subUnitId
) {
this._activePopup = location;
this._activePopup$.next(location);
return;
}
if (this._lastPopup) {
this._lastPopup.dispose();
};
if (this._zenZoneService.visible) {
return;
}
this._activePopup = location;
this._activePopup$.next(location);
const popupDisposable = this._cellPopupManagerService.showPopup(
{
unitId,
subUnitId,
row,
col,
},
{
componentKey: SHEET_NOTE_COMPONENT,
onClickOutside: () => {
this.hidePopup();
},
direction: 'horizontal',
extraProps: {
location,
},
priority: 3,
}
);
if (!popupDisposable) {
throw new Error('[SheetsNotePopupService]: cannot show popup!');
}
const disposableCollection = new DisposableCollection();
disposableCollection.add(popupDisposable);
disposableCollection.add({
dispose: () => {
onHide?.();
},
});
this._lastPopup = disposableCollection;
}
hidePopup(force?: boolean) {
if (!this._activePopup) {
return;
}
if (!force && !this._activePopup.temp) return;
if (this._lastPopup) {
this._lastPopup.dispose();
}
this._lastPopup = null;
this._activePopup = null;
this._activePopup$.next(null);
}
persistPopup() {
if (!this._activePopup || !this._activePopup.temp) {
return;
}
this._activePopup = {
...this._activePopup,
temp: false,
};
this._activePopup$.next(this._activePopup);
}
}
@@ -0,0 +1,17 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SHEET_NOTE_COMPONENT = 'SHEET_NOTE_COMPONENT';
+126
View File
@@ -0,0 +1,126 @@
/**
* 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 { ISheetLocationBase } from '@univerjs/sheets';
import type { ISheetNote } from '@univerjs/sheets-note';
import type { IPopup } from '@univerjs/ui';
import type { IUniverSheetsNoteUIPluginConfig } from '../plugin';
import { ICommandService, LocaleService } from '@univerjs/core';
import { SheetDeleteNoteCommand, SheetsNoteModel, SheetUpdateNoteCommand } from '@univerjs/sheets-note';
import { useConfigValue, useDebounceFn, useDependency } from '@univerjs/ui';
import { useEffect, useMemo, useRef, useState } from 'react';
import { of } from 'rxjs';
import { SHEETS_NOTE_UI_PLUGIN_CONFIG_KEY } from '../plugin';
export const SheetsNote = (props: { popup: IPopup<{ location: ISheetLocationBase }> }) => {
const noteModel = useDependency(SheetsNoteModel);
const localeService = useDependency(LocaleService);
const config = useConfigValue<IUniverSheetsNoteUIPluginConfig>(SHEETS_NOTE_UI_PLUGIN_CONFIG_KEY);
const [note, setNote] = useState<ISheetNote>({ width: config?.defaultNoteSize?.width || 216, height: config?.defaultNoteSize?.height || 92, note: '' });
const activePopup = props.popup.extraProps?.location;
const textareaRef = useRef<HTMLTextAreaElement>(null);
const commandService = useDependency(ICommandService);
const cellNoteChange$ = useMemo(() => activePopup ? noteModel.getCellNoteChange$(activePopup.unitId, activePopup.subUnitId!, activePopup.row, activePopup.col) : of(null), [activePopup]);
const updateNote = useDebounceFn((newNote: ISheetNote) => {
if (!activePopup) return;
if (newNote.note) {
commandService.executeCommand(SheetUpdateNoteCommand.id, {
unitId: activePopup.unitId,
sheetId: activePopup.subUnitId!,
row: activePopup.row,
col: activePopup.col,
note: newNote,
});
} else {
commandService.executeCommand(SheetDeleteNoteCommand.id, {
unitId: activePopup.unitId,
sheetId: activePopup.subUnitId!,
row: activePopup.row,
col: activePopup.col,
});
}
});
useEffect(() => {
if (activePopup) {
const existingNote = noteModel.getNote(activePopup.unitId, activePopup.subUnitId!, activePopup.row, activePopup.col);
if (existingNote) {
setNote(existingNote);
} else {
textareaRef.current?.focus();
}
}
}, [activePopup, noteModel]);
useEffect(() => {
const sub = cellNoteChange$.subscribe(() => {
if (!activePopup) return;
const existingNote = noteModel.getNote(activePopup.unitId, activePopup.subUnitId!, activePopup.row, activePopup.col);
if (existingNote) {
setNote(existingNote);
}
});
return () => sub.unsubscribe();
}, [cellNoteChange$]);
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) return;
const { width, height } = entry.contentRect;
const newNote = { ...note, width, height };
setNote(newNote);
updateNote(newNote);
});
resizeObserver.observe(textarea);
return () => {
resizeObserver.disconnect();
};
}, [note.note]); // Only re-run when note content changes
const handleNoteChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const noteString = e.target.value;
const newNote = { ...note, note: noteString };
setNote(newNote);
updateNote(newNote);
};
if (!activePopup) return null;
return (
<textarea
style={{ width: note.width, height: note.height }}
ref={textareaRef}
className={`
univer-note-textarea univer-resize-both univer-ml-[1px] univer-rounded univer-border univer-border-solid
univer-border-gray-200 univer-p-2 univer-shadow
focus:univer-outline-none
`}
value={note.note}
onChange={handleNoteChange}
placeholder={localeService.t('note.placeholder')}
/>
);
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,27 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Config } from 'tailwindcss';
import preset from '@univerjs-infra/shared/tailwind';
const config: Config = {
presets: [preset],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
],
};
export default config;
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src"]
}
@@ -0,0 +1,4 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/node",
"include": ["vitest.config.ts"]
}
+3
View File
@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();
+28
View File
@@ -0,0 +1,28 @@
# @univerjs/sheets-note
| Package | Description | Version | License | Downloads | Continuous Integration | Coverage |
|---------|-------------|---------|----------|-----------|----------------------|----------|
| `@univerjs/sheets-note` | `UniverSheetsNote` | [![][npm-version-shield]][npm-version-link] | ![][npm-license-shield] | ![][npm-downloads-shield] | ❌ | ❌ |
## Introduction
`@univerjs/sheets-note` core package for sheet note. For using, please refer to [@univerjs/sheets-note](../sheets-note/README.md)
## Usage
### Installation
```shell
npm install @univerjs/sheets-note
```
or
```shell
pnpm add @univerjs/sheets-note
```
[npm-version-shield]: https://img.shields.io/npm/v/@univerjs/sheets-note?style=flat-square
[npm-version-link]: https://npmjs.com/package/@univerjs/sheets-note
[npm-license-shield]: https://img.shields.io/npm/l/@univerjs/sheets-note?style=flat-square
[npm-downloads-shield]: https://img.shields.io/npm/dm/@univerjs/sheets-note?style=flat-square
+83
View File
@@ -0,0 +1,83 @@
{
"name": "@univerjs/sheets-note",
"version": "0.6.10",
"private": false,
"description": "Univer sheets note base plugin",
"author": "DreamNum <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": [],
"exports": {
".": "./src/index.ts",
"./*": "./src/*",
"./facade": "./src/facade/index.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
"publishConfig": {
"access": "public",
"main": "./lib/es/index.js",
"module": "./lib/es/index.js",
"exports": {
".": {
"import": "./lib/es/index.js",
"require": "./lib/cjs/index.js",
"types": "./lib/types/index.d.ts"
},
"./*": {
"import": "./lib/es/*",
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./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",
"lint:types": "tsc --noEmit",
"build": "univer-cli build"
},
"peerDependencies": {
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/sheets": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"rxjs": "^7.8.1",
"typescript": "^5.8.3",
"vite": "^6.3.3",
"vitest": "^3.1.2"
}
}
@@ -0,0 +1,73 @@
/**
* 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 { IUpdateNoteMutationParams } from '../mutations/note.mutation';
import { CommandType, ICommandService, IUniverInstanceService } from '@univerjs/core';
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
import { RemoveNoteMutation, ToggleNotePopupMutation, UpdateNoteMutation } from '../mutations/note.mutation';
export const SheetDeleteNoteCommand: ICommand = {
id: 'sheet.command.delete-note',
type: CommandType.COMMAND,
handler: (accessor, params) => {
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
const selection = sheetsSelectionsService.getCurrentLastSelection();
if (!selection?.primary) return false;
const { actualColumn, actualRow } = selection.primary;
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(RemoveNoteMutation.id, {
unitId: target.unitId,
sheetId: target.subUnitId,
row: actualRow,
col: actualColumn,
});
},
};
export const SheetToggleNotePopupCommand: ICommand = {
id: 'sheet.command.toggle-note-popup',
type: CommandType.COMMAND,
handler: (accessor, params) => {
const univerInstanceService = accessor.get(IUniverInstanceService);
const target = getSheetCommandTarget(univerInstanceService);
if (!target) return false;
const sheetsSelectionsService = accessor.get(SheetsSelectionsService);
const selection = sheetsSelectionsService.getCurrentLastSelection();
if (!selection?.primary) return false;
const { actualColumn, actualRow } = selection.primary;
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(ToggleNotePopupMutation.id, {
unitId: target.unitId,
sheetId: target.subUnitId,
row: actualRow,
col: actualColumn,
});
},
};
export const SheetUpdateNoteCommand: ICommand<IUpdateNoteMutationParams> = {
id: 'sheet.command.update-note',
type: CommandType.COMMAND,
handler: (accessor, params) => {
const commandService = accessor.get(ICommandService);
return commandService.syncExecuteCommand(UpdateNoteMutation.id, params);
},
};
@@ -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 { IAccessor, IMutation } from '@univerjs/core';
import type { ISheetNote } from '../../models/sheets-note.model';
import { CommandType } from '@univerjs/core';
import { SheetsNoteModel } from '../../models/sheets-note.model';
export interface IUpdateNoteMutationParams {
unitId: string;
sheetId: string;
row: number;
col: number;
note: ISheetNote;
silent?: boolean;
}
export const UpdateNoteMutation: IMutation<IUpdateNoteMutationParams> = {
id: 'sheet.mutation.update-note',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IUpdateNoteMutationParams) => {
const { unitId, sheetId, row, col, note, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.updateNote(unitId, sheetId, row, col, note, silent);
return true;
},
};
export interface IRemoveNoteMutationParams {
unitId: string;
sheetId: string;
row: number;
col: number;
silent?: boolean;
}
export const RemoveNoteMutation: IMutation<IRemoveNoteMutationParams> = {
id: 'sheet.mutation.remove-note',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IRemoveNoteMutationParams) => {
const { unitId, sheetId, row, col, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.removeNote(unitId, sheetId, row, col, silent);
return true;
},
};
export interface IToggleNotePopupMutationParams {
unitId: string;
sheetId: string;
row: number;
col: number;
silent?: boolean;
}
export const ToggleNotePopupMutation: IMutation<IToggleNotePopupMutationParams> = {
id: 'sheet.mutation.toggle-note-popup',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IToggleNotePopupMutationParams) => {
const { unitId, sheetId, row, col, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.toggleNotePopup(unitId, sheetId, row, col, silent);
return true;
},
};
export interface IUpdateNotePositionMutationParams {
unitId: string;
sheetId: string;
row: number;
col: number;
newPosition: {
row: number;
col: number;
};
silent?: boolean;
}
export const UpdateNotePositionMutation: IMutation<IUpdateNotePositionMutationParams> = {
id: 'sheet.mutation.update-note-position',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: IUpdateNotePositionMutationParams) => {
const { unitId, sheetId, row, col, newPosition, silent } = params;
const sheetsNoteModel = accessor.get(SheetsNoteModel);
sheetsNoteModel.updateNotePosition(unitId, sheetId, row, col, newPosition.row, newPosition.col, silent);
return true;
},
};
@@ -0,0 +1,205 @@
/**
* 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 { ISheetNote } from '../models/sheets-note.model';
import { Disposable, ICommandService, Inject, sequenceExecuteAsync } from '@univerjs/core';
import { handleCommonRangeChangeWithEffectRefCommandsSkipNoInterests, RefRangeService, SheetsSelectionsService } from '@univerjs/sheets';
import { RemoveNoteMutation, UpdateNoteMutation, UpdateNotePositionMutation } from '../commands/mutations/note.mutation';
import { SheetsNoteModel } from '../models/sheets-note.model';
export class SheetsNoteRefRangeController extends Disposable {
private _disposableMap = new Map<string, IDisposable>();
private _watcherMap = new Map<string, IDisposable>();
constructor(
@Inject(RefRangeService) private readonly _refRangeService: RefRangeService,
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel,
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initData();
this._initRefRange();
}
private _getIdWithUnitId(unitId: string, subUnitId: string, row: number, col: number) {
return `${unitId}-${subUnitId}-${row}-${col}`;
}
private _handleRangeChange = (unitId: string, subUnitId: string, note: ISheetNote, row: number, col: number, resultRange: Nullable<IRange>, silent?: boolean) => {
if (!resultRange) {
return {
redos: [{
id: RemoveNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
},
}],
undos: [{
id: UpdateNoteMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
note,
},
}],
};
}
return {
redos: [{
id: UpdateNotePositionMutation.id,
params: {
unitId,
sheetId: subUnitId,
row,
col,
newPosition: {
row: resultRange.startRow,
col: resultRange.startColumn,
},
silent,
},
}],
undos: [{
id: UpdateNotePositionMutation.id,
params: {
unitId,
sheetId: subUnitId,
row: resultRange.startRow,
col: resultRange.startColumn,
newPosition: {
row,
col,
},
note,
silent,
},
}],
};
};
private _register(unitId: string, subUnitId: string, note: ISheetNote, row: number, col: number) {
const oldRange: IRange = {
startColumn: col,
endColumn: col,
startRow: row,
endRow: row,
};
this._disposableMap.set(
this._getIdWithUnitId(unitId, subUnitId, row, col),
this._refRangeService.registerRefRange(oldRange, (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._handleRangeChange(unitId, subUnitId, note, row, col, resultRange, false);
return res;
}, unitId, subUnitId)
);
}
private _watch(unitId: string, subUnitId: string, note: ISheetNote, row: number, col: number) {
const oldRange: IRange = {
startColumn: col,
endColumn: col,
startRow: row,
endRow: row,
};
this._watcherMap.set(
this._getIdWithUnitId(unitId, subUnitId, row, col),
this._refRangeService.watchRange(unitId, subUnitId, oldRange, (before, after) => {
const { redos } = this._handleRangeChange(unitId, subUnitId, note, before.startRow, before.startColumn, after, true);
sequenceExecuteAsync(redos, this._commandService, { onlyLocal: true });
}, true)
);
}
private _unwatch(unitId: string, subUnitId: string, row: number, col: number) {
const id = this._getIdWithUnitId(unitId, subUnitId, row, col);
this._watcherMap.get(id)?.dispose();
this._watcherMap.delete(id);
}
private _unregister(unitId: string, subUnitId: string, row: number, col: number) {
const id = this._getIdWithUnitId(unitId, subUnitId, row, col);
this._disposableMap.get(id)?.dispose();
this._disposableMap.delete(id);
}
private _initData() {
const unitNotes = this._sheetsNoteModel.getNotes();
for (const [unitId, unitNote] of unitNotes) {
for (const [sheetId, matrix] of unitNote) {
matrix.forValue((row: number, col: number, value: ISheetNote) => {
if (value) {
this._register(unitId, sheetId, value, row, col);
this._watch(unitId, sheetId, value, row, col);
}
return true;
});
}
}
}
private _initRefRange() {
this.disposeWithMe(
this._sheetsNoteModel.change$.subscribe((option) => {
switch (option.type) {
case 'update': {
const { unitId, sheetId, row, col, note } = option;
const id = this._getIdWithUnitId(unitId, sheetId, row, col);
if (note) {
if (!this._disposableMap.has(id)) {
this._register(unitId, sheetId, note, row, col);
this._watch(unitId, sheetId, note, row, col);
}
} else {
this._unregister(unitId, sheetId, row, col);
this._unwatch(unitId, sheetId, row, col);
}
break;
}
case 'ref': {
const { unitId, sheetId, row, col, newPosition, note, silent } = option;
this._unregister(unitId, sheetId, row, col);
if (!silent) {
this._unwatch(unitId, sheetId, row, col);
this._watch(unitId, sheetId, note, newPosition.row, newPosition.col);
}
this._register(unitId, sheetId, note, newPosition.row, newPosition.col);
break;
}
}
})
);
}
}
@@ -0,0 +1,104 @@
/**
* 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 { ISheetNote } from '../models/sheets-note.model';
import { Disposable, Inject, IResourceManagerService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { SheetsNoteModel } from '../models/sheets-note.model';
import { PLUGIN_NAME } from '../plugin';
interface INoteData {
[sheetId: string]: {
[row: number]: {
[col: number]: ISheetNote;
};
};
}
export class SheetsNoteResourceController extends Disposable {
constructor(
@IResourceManagerService private readonly _resourceManagerService: IResourceManagerService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(SheetsNoteModel) private readonly _sheetsNoteModel: SheetsNoteModel
) {
super();
this._initSnapshot();
}
private _initSnapshot() {
const toJson = (unitId: string) => {
const unitMap = this._sheetsNoteModel.getUnitNotes(unitId);
if (!unitMap) {
return '';
}
const result: INoteData = {};
unitMap.forEach((matrix, sheetId) => {
const sheetNotes: INoteData[string] = {};
matrix.forValue((row, col, note) => {
if (!sheetNotes[row]) {
sheetNotes[row] = {};
}
sheetNotes[row][col] = note;
});
if (Object.keys(sheetNotes).length > 0) {
result[sheetId] = sheetNotes;
}
});
return JSON.stringify(result);
};
const parseJson = (json: string): INoteData => {
if (!json) {
return {};
}
try {
return JSON.parse(json);
} catch (err) {
return {};
}
};
this.disposeWithMe(
this._resourceManagerService.registerPluginResource<INoteData>({
pluginName: PLUGIN_NAME,
businesses: [UniverInstanceType.UNIVER_SHEET],
toJson: (unitId) => toJson(unitId),
parseJson: (json) => parseJson(json),
onUnLoad: (unitId) => {
// Clear all notes for this unit
this._sheetsNoteModel.deleteUnitNotes(unitId);
},
onLoad: (unitId, value) => {
// Load notes from the parsed data
Object.entries(value).forEach(([sheetId, sheetNotes]) => {
Object.entries(sheetNotes).forEach(([row, colNotes]) => {
Object.entries(colNotes).forEach(([col, note]) => {
this._sheetsNoteModel.updateNote(
unitId,
sheetId,
Number(row),
Number(col),
note
);
});
});
});
},
})
);
}
}
@@ -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.
*/
import { Disposable, ICommandService } from '@univerjs/core';
import { SheetDeleteNoteCommand, SheetToggleNotePopupCommand, SheetUpdateNoteCommand } from '../commands/commands/note.command';
import { RemoveNoteMutation, ToggleNotePopupMutation, UpdateNoteMutation, UpdateNotePositionMutation } from '../commands/mutations/note.mutation';
export class SheetsNoteController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService
) {
super();
this._initialize();
}
private _initialize(): void {
[
UpdateNotePositionMutation,
ToggleNotePopupMutation,
UpdateNoteMutation,
RemoveNoteMutation,
SheetDeleteNoteCommand,
SheetToggleNotePopupCommand,
SheetUpdateNoteCommand,
].forEach((command) => {
this.disposeWithMe(
this._commandService.registerCommand(command)
);
});
}
}
+279
View File
@@ -0,0 +1,279 @@
/**
* 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 { ISheetNote } from '@univerjs/sheets-note';
import type { FWorkbook, FWorksheet } from '@univerjs/sheets/facade';
import { FEventName } from '@univerjs/core/facade';
export interface ISheetNoteAddEventParmas {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
note: ISheetNote;
cancel?: boolean;
}
export interface ISheetNoteDeleteEventParmas {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
oldNote: ISheetNote;
cancel?: boolean;
}
export interface ISheetNoteUpdateEventParmas {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
note: ISheetNote;
oldNote: ISheetNote;
cancel?: boolean;
}
export interface ISheetNoteShowEventParmas {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
cancel?: boolean;
}
export interface ISheetNoteHideEventParmas {
workbook: FWorkbook;
worksheet: FWorksheet;
row: number;
col: number;
cancel?: boolean;
}
/**
* @ignore
*/
export interface ISheetsNoteEventParamConfig {
SheetNoteAdd: ISheetNoteAddEventParmas;
SheetNoteDelete: ISheetNoteDeleteEventParmas;
SheetNoteUpdate: ISheetNoteUpdateEventParmas;
SheetNoteShow: ISheetNoteShowEventParmas;
SheetNoteHide: ISheetNoteHideEventParmas;
BeforeSheetNoteAdd: ISheetNoteAddEventParmas;
BeforeSheetNoteDelete: ISheetNoteDeleteEventParmas;
BeforeSheetNoteUpdate: ISheetNoteUpdateEventParmas;
BeforeSheetNoteShow: ISheetNoteShowEventParmas;
BeforeSheetNoteHide: ISheetNoteHideEventParmas;
}
/**
* @ignore
*/
interface ISheetNoteEvent {
/**
* Event fired when a note is added
* @see {@link ISheetNoteAddEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteAdd, (params) => {
* const { workbook, worksheet, row, col, note } = params;
* console.log(params);
* });
* ```
*/
SheetNoteAdd: 'SheetNoteAdd';
/**
* Event fired when a note is deleted
* @see {@link ISheetNoteDeleteEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteDelete, (params) => {
* const { workbook, worksheet, row, col, oldNote } = params;
* console.log(params);
* });
* ```
*/
SheetNoteDelete: 'SheetNoteDelete';
/**
* Event fired when a note is updated
* @see {@link ISheetNoteUpdateEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteUpdate, (params) => {
* const { workbook, worksheet, row, col, note, oldNote } = params;
* console.log(params);
* });
* ```
*/
SheetNoteUpdate: 'SheetNoteUpdate';
/**
* Event fired when a note is shown
* @see {@link ISheetNoteShowEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteShow, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
* });
* ```
*/
SheetNoteShow: 'SheetNoteShow';
/**
* Event fired when a note is hidden
* @see {@link ISheetNoteHideEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.SheetNoteHide, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
* });
* ```
*/
SheetNoteHide: 'SheetNoteHide';
/**
* Event fired before a note is added
* @see {@link ISheetNoteAddEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteAdd, (params) => {
* const { workbook, worksheet, row, col, note } = params;
* console.log(params);
* });
* ```
*/
BeforeSheetNoteAdd: 'BeforeSheetNoteAdd';
/**
* Event fired before a note is deleted
* @see {@link ISheetNoteDeleteEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteDelete, (params) => {
* const { workbook, worksheet, row, col, oldNote } = params;
* console.log(params);
* });
* ```
*/
BeforeSheetNoteDelete: 'BeforeSheetNoteDelete';
/**
* Event fired before a note is updated
* @see {@link ISheetNoteUpdateEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteUpdate, (params) => {
* const { workbook, worksheet, row, col, note, oldNote } = params;
* console.log(params);
* });
* ```
*/
BeforeSheetNoteUpdate: 'BeforeSheetNoteUpdate';
/**
* Event fired before a note is shown
* @see {@link ISheetNoteShowEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteShow, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
* });
* ```
*/
BeforeSheetNoteShow: 'BeforeSheetNoteShow';
/**
* Event fired before a note is hidden
* @see {@link ISheetNoteHideEventParmas}
* @example
* ```ts
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeSheetNoteHide, (params) => {
* const { workbook, worksheet, row, col } = params;
* console.log(params);
* });
* ```
*/
BeforeSheetNoteHide: 'BeforeSheetNoteHide';
}
/**
* @ignore
*/
export class FSheetNoteEvent implements ISheetNoteEvent {
get SheetNoteAdd(): 'SheetNoteAdd' {
return 'SheetNoteAdd';
}
get SheetNoteDelete(): 'SheetNoteDelete' {
return 'SheetNoteDelete';
}
get SheetNoteUpdate(): 'SheetNoteUpdate' {
return 'SheetNoteUpdate';
}
get SheetNoteShow(): 'SheetNoteShow' {
return 'SheetNoteShow';
}
get SheetNoteHide(): 'SheetNoteHide' {
return 'SheetNoteHide';
}
get BeforeSheetNoteAdd(): 'BeforeSheetNoteAdd' {
return 'BeforeSheetNoteAdd';
}
get BeforeSheetNoteDelete(): 'BeforeSheetNoteDelete' {
return 'BeforeSheetNoteDelete';
}
get BeforeSheetNoteUpdate(): 'BeforeSheetNoteUpdate' {
return 'BeforeSheetNoteUpdate';
}
get BeforeSheetNoteShow(): 'BeforeSheetNoteShow' {
return 'BeforeSheetNoteShow';
}
get BeforeSheetNoteHide(): 'BeforeSheetNoteHide' {
return 'BeforeSheetNoteHide';
}
}
/**
* @ignore
*/
export interface ISheetNoteEventConfig {
SheetNoteAdd: ISheetNoteAddEventParmas;
SheetNoteDelete: ISheetNoteDeleteEventParmas;
SheetNoteUpdate: ISheetNoteUpdateEventParmas;
SheetNoteShow: ISheetNoteShowEventParmas;
SheetNoteHide: ISheetNoteHideEventParmas;
BeforeSheetNoteAdd: ISheetNoteAddEventParmas;
BeforeSheetNoteDelete: ISheetNoteDeleteEventParmas;
BeforeSheetNoteUpdate: ISheetNoteUpdateEventParmas;
BeforeSheetNoteShow: ISheetNoteShowEventParmas;
BeforeSheetNoteHide: ISheetNoteHideEventParmas;
}
FEventName.extend(FSheetNoteEvent);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEventName extends ISheetNoteEvent {
}
interface IEventParamConfig extends ISheetNoteEventConfig {
}
}
+103
View File
@@ -0,0 +1,103 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Nullable } from '@univerjs/core';
import type { ISheetNote } from '@univerjs/sheets-note';
import { RemoveNoteMutation, SheetsNoteModel, UpdateNoteMutation } from '@univerjs/sheets-note';
import { FRange } from '@univerjs/sheets/facade';
export interface IFSheetsNoteRange {
/**
* get the note of the primary cell
* @returns {Nullable<ISheetNote>} the note of the primary cell
* @example
* ```ts
* const fWorkbook = univerAPI.getWorkbook();
* const fWorksheet = fWorkbook.getWorksheet();
* const fRange = fWorksheet.getRange(0, 0, 1, 1);
* const note = fRange.getNote();
* ```
*/
getNote(): Nullable<ISheetNote>;
/**
* create a new note and attach to the primary cell
* @param {ISheetNote} note the note to create
* @returns {FRange} the range
* @example
* ```ts
* const fWorkbook = univerAPI.getWorkbook();
* const fWorksheet = fWorkbook.getWorksheet();
* const fRange = fWorksheet.getRange(0, 0, 1, 1);
* const note = fRange.createOrUpdateNote({});
* ```
*/
createOrUpdateNote(note: ISheetNote): FRange;
/**
* delete the note of the primary cell
* @returns {FRange} the range
* @example
* ```ts
* const fWorkbook = univerAPI.getWorkbook();
* const fWorksheet = fWorkbook.getWorksheet();
* const fRange = fWorksheet.getRange(0, 0, 1, 1);
* fRange.deleteNote();
* ```
*/
deleteNote(): FRange;
}
export class FSheetsNoteRange extends FRange implements IFSheetsNoteRange {
createOrUpdateNote(note: ISheetNote): FRange {
this._commandService.syncExecuteCommand(
UpdateNoteMutation.id,
{
unitId: this.getUnitId(),
sheetId: this.getSheetId(),
row: this.getRow(),
col: this.getColumn(),
note,
}
);
return this;
}
deleteNote(): FRange {
const model = this._injector.get(SheetsNoteModel);
this._commandService.syncExecuteCommand(
RemoveNoteMutation.id,
{
unitId: this.getUnitId(),
sheetId: this.getSheetId(),
row: this.getRow(),
col: this.getColumn(),
}
);
return this;
}
getNote(): Nullable<ISheetNote> {
const model = this._injector.get(SheetsNoteModel);
return model.getNote(this.getUnitId(), this.getSheetId(), this.getRow(), this.getColumn());
}
}
declare module '@univerjs/sheets/facade' {
interface IRange extends FSheetsNoteRange { }
}
FRange.extend(FSheetsNoteRange);
+311
View File
@@ -0,0 +1,311 @@
/**
* 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 { IRemoveNoteMutationParams, IUpdateNoteMutationParams } from '@univerjs/sheets-note';
import { CanceledError, ICommandService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { SheetDeleteNoteCommand, SheetsNoteModel, SheetToggleNotePopupCommand, SheetUpdateNoteCommand } from '@univerjs/sheets-note';
/**
* @ignore
*/
export interface IFUniverSheetNoteMixin {
// Add any note-specific methods here if needed
}
export class FUniverSheetNoteMixin extends FUniver implements IFUniverSheetNoteMixin {
override _initialize(injector: Injector): void {
this.registerEventHandler(
this.Event.SheetNoteAdd,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && !change.oldNote && change.note) {
const { unitId, sheetId, row, col, note, oldNote } = change;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
this.fireEvent(this.Event.SheetNoteAdd, {
workbook,
worksheet,
row,
col,
note,
});
}
});
}
);
this.registerEventHandler(
this.Event.SheetNoteDelete,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && !change.note) {
const { unitId, sheetId, row, col, note, oldNote } = change;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
this.fireEvent(this.Event.SheetNoteDelete, {
workbook,
worksheet,
row,
col,
oldNote,
});
}
});
}
);
this.registerEventHandler(
this.Event.SheetNoteUpdate,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && change.note) {
const { unitId, sheetId, row, col, note, oldNote } = change;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
this.fireEvent(this.Event.SheetNoteUpdate, {
workbook,
worksheet,
row,
col,
note,
oldNote,
});
}
});
}
);
this.registerEventHandler(
this.Event.SheetNoteShow,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && change.note && !change.oldNote.show && change.note.show) {
const { unitId, sheetId, row, col } = change;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
this.fireEvent(this.Event.SheetNoteShow, {
workbook,
worksheet,
row,
col,
});
}
});
}
);
this.registerEventHandler(
this.Event.SheetNoteHide,
() => {
const model = injector.get(SheetsNoteModel);
return model.change$.subscribe((change) => {
if (change.type === 'update' && change.oldNote && change.note && change.oldNote.show && !change.note.show) {
const { unitId, sheetId, row, col } = change;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
this.fireEvent(this.Event.SheetNoteHide, {
workbook,
worksheet,
row,
col,
});
}
});
}
);
this.registerEventHandler(
this.Event.BeforeSheetNoteAdd,
() => {
const commandService = injector.get(ICommandService);
return commandService.beforeCommandExecuted((command) => {
if (command.id === SheetUpdateNoteCommand.id) {
const model = injector.get(SheetsNoteModel);
const { unitId, sheetId, row, col, note } = command.params as IUpdateNoteMutationParams;
const oldNote = model.getNote(unitId, sheetId, row, col);
if (oldNote) return;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
const cancel = this.fireEvent(this.Event.BeforeSheetNoteAdd, {
workbook,
worksheet,
row,
col,
note,
});
if (cancel) {
throw new CanceledError();
}
}
});
}
);
this.registerEventHandler(
this.Event.BeforeSheetNoteDelete,
() => {
const commandService = injector.get(ICommandService);
return commandService.beforeCommandExecuted((command) => {
if (command.id === SheetDeleteNoteCommand.id) {
const model = injector.get(SheetsNoteModel);
const { unitId, sheetId, row, col } = command.params as IRemoveNoteMutationParams;
const oldNote = model.getNote(unitId, sheetId, row, col);
if (!oldNote) return;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
const cancel = this.fireEvent(this.Event.BeforeSheetNoteDelete, {
workbook,
worksheet,
row,
col,
oldNote,
});
if (cancel) {
throw new CanceledError();
}
}
});
}
);
this.registerEventHandler(
this.Event.BeforeSheetNoteUpdate,
() => {
const commandService = injector.get(ICommandService);
return commandService.beforeCommandExecuted((command) => {
if (command.id === SheetUpdateNoteCommand.id) {
const model = injector.get(SheetsNoteModel);
const { unitId, sheetId, row, col, note } = command.params as IUpdateNoteMutationParams;
const oldNote = model.getNote(unitId, sheetId, row, col);
if (!oldNote) return;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
const cancel = this.fireEvent(this.Event.BeforeSheetNoteUpdate, {
workbook,
worksheet,
row,
col,
note,
oldNote,
});
if (cancel) {
throw new CanceledError();
}
}
});
}
);
this.registerEventHandler(
this.Event.BeforeSheetNoteShow,
() => {
const commandService = injector.get(ICommandService);
return commandService.beforeCommandExecuted((command) => {
if (command.id === SheetToggleNotePopupCommand.id) {
const model = injector.get(SheetsNoteModel);
const { unitId, sheetId, row, col } = command.params as IUpdateNoteMutationParams;
const oldNote = model.getNote(unitId, sheetId, row, col);
if (oldNote?.show) return;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
const cancel = this.fireEvent(this.Event.BeforeSheetNoteShow, {
workbook,
worksheet,
row,
col,
});
if (cancel) {
throw new CanceledError();
}
}
});
}
);
this.registerEventHandler(
this.Event.BeforeSheetNoteHide,
() => {
const commandService = injector.get(ICommandService);
return commandService.beforeCommandExecuted((command) => {
if (command.id === SheetToggleNotePopupCommand.id) {
const model = injector.get(SheetsNoteModel);
const { unitId, sheetId, row, col } = command.params as IUpdateNoteMutationParams;
const oldNote = model.getNote(unitId, sheetId, row, col);
if (!oldNote?.show) return;
const target = this.getSheetTarget(unitId, sheetId);
if (!target) {
return;
}
const { workbook, worksheet } = target;
const cancel = this.fireEvent(this.Event.BeforeSheetNoteHide, {
workbook,
worksheet,
row,
col,
});
if (cancel) {
throw new CanceledError();
}
}
});
}
);
}
}
FUniver.extend(FUniverSheetNoteMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FUniver extends IFUniverSheetNoteMixin {}
}
@@ -0,0 +1,55 @@
/**
* 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 { ISheetNote } from '@univerjs/sheets-note';
import { SheetsNoteModel } from '@univerjs/sheets-note';
import { FWorksheet } from '@univerjs/sheets/facade';
export interface ISheetNoteInfo extends ISheetNote {
row: number;
col: number;
}
export interface IFSheetsNoteWorksheet {
/**
* get the note of the primary cell
*/
getNotes(): ISheetNoteInfo[];
}
export class FSheetsNoteWorksheet extends FWorksheet implements IFSheetsNoteWorksheet {
getNotes(): ISheetNoteInfo[] {
const model = this._injector.get(SheetsNoteModel);
const notes = model.getSheetNotes(this.getWorkbook().getUnitId(), this.getSheetId());
const arr: ISheetNoteInfo[] = [];
notes?.forValue((row, col, note) => {
arr.push({
...note,
row,
col,
});
});
return arr;
}
}
declare module '@univerjs/sheets/facade' {
interface IWorksheet extends FSheetsNoteWorksheet { }
}
FWorksheet.extend(FSheetsNoteWorksheet);
+25
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.
*/
import './f-event';
import './f-range';
import './f-univer';
import './f-worksheet';
export * from './f-event';
export * from './f-range';
export * from './f-univer';
export * from './f-worksheet';
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './plugin';
export * from './models/sheets-note.model';
export * from './controllers/sheets-note-resource.controller';
export * from './commands/mutations/note.mutation';
export * from './commands/commands/note.command';
@@ -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 { Nullable } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { Disposable, ObjectMatrix } from '@univerjs/core';
import { filter, map, Subject } from 'rxjs';
export interface ISheetNote {
width: number;
height: number;
note: string;
show?: boolean;
}
export type ISheetNoteChange = {
unitId: string;
sheetId: string;
row: number;
col: number;
silent?: boolean;
} & ({
type: 'update';
note: Nullable<ISheetNote>;
oldNote: Nullable<ISheetNote>;
} | {
type: 'ref';
newPosition: {
row: number;
col: number;
};
note: ISheetNote;
});
export class SheetsNoteModel extends Disposable {
private _noteMatrix = new Map<string, Map<string, ObjectMatrix<ISheetNote>>>();
private readonly _change$ = new Subject<ISheetNoteChange>();
readonly change$ = this._change$.asObservable();
private _ensureNoteMatrix(unitId: string, sheetId: string): ObjectMatrix<ISheetNote> {
let unitMap = this._noteMatrix.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._noteMatrix.set(unitId, unitMap);
}
let matrix = unitMap.get(sheetId);
if (!matrix) {
matrix = new ObjectMatrix<ISheetNote>();
unitMap.set(sheetId, matrix);
}
return matrix;
}
getSheetShowNotes$(unitId: string, sheetId: string) {
return this._change$.pipe(
filter(({ unitId: u, sheetId: s }) => u === unitId && s === sheetId),
map(() => {
const matrix = this._ensureNoteMatrix(unitId, sheetId);
const notes: { loc: ISheetLocationBase; note: ISheetNote }[] = [];
matrix.forValue((row, col, note) => {
if (note.show) {
notes.push({ loc: { row, col, unitId, subUnitId: sheetId }, note });
}
});
return notes;
})
);
}
getCellNoteChange$(unitId: string, sheetId: string, row: number, col: number) {
return this._change$.pipe(
filter(({ unitId: u, sheetId: s, row: r, col: c }) => u === unitId && s === sheetId && r === row && c === col),
map(({ note }) => note)
);
}
updateNote(unitId: string, sheetId: string, row: number, col: number, note: ISheetNote, silent?: boolean): void {
const matrix = this._ensureNoteMatrix(unitId, sheetId);
const oldNote = matrix.getValue(row, col);
matrix.setValue(row, col, note);
this._change$.next({ unitId, sheetId, row, col, type: 'update', note, oldNote, silent });
}
removeNote(unitId: string, sheetId: string, row: number, col: number, silent?: boolean): void {
const matrix = this._ensureNoteMatrix(unitId, sheetId);
const oldNote = matrix.getValue(row, col);
matrix.realDeleteValue(row, col);
this._change$.next({ unitId, sheetId, row, col, type: 'update', note: null, oldNote, silent });
}
toggleNotePopup(unitId: string, sheetId: string, row: number, col: number, silent?: boolean): void {
const matrix = this._ensureNoteMatrix(unitId, sheetId);
const note = matrix.getValue(row, col);
if (note) {
note.show = !note.show;
const newNote = { ...note, show: note.show };
matrix.setValue(row, col, newNote);
this._change$.next({
unitId,
sheetId,
row,
col,
type: 'update',
note: newNote,
oldNote: note,
silent,
});
}
}
updateNotePosition(unitId: string, sheetId: string, row: number, col: number, newRow: number, newCol: number, silent?: boolean): void {
const matrix = this._ensureNoteMatrix(unitId, sheetId);
const note = matrix.getValue(row, col);
if (note) {
matrix.realDeleteValue(row, col);
matrix.setValue(newRow, newCol, note);
this._change$.next({
unitId,
sheetId,
row,
col,
type: 'ref',
newPosition: { row: newRow, col: newCol },
note,
silent,
});
}
}
getNote(unitId: string, sheetId: string, row: number, col: number): Nullable<ISheetNote> {
const matrix = this._ensureNoteMatrix(unitId, sheetId);
return matrix.getValue(row, col);
}
getUnitNotes(unitId: string): Map<string, ObjectMatrix<ISheetNote>> | undefined {
return this._noteMatrix.get(unitId);
}
getSheetNotes(unitId: string, sheetId: string): ObjectMatrix<ISheetNote> | undefined {
const unitMap = this._noteMatrix.get(unitId);
if (!unitMap) {
return undefined;
}
return unitMap.get(sheetId);
}
getNotes() {
return this._noteMatrix;
}
deleteUnitNotes(unitId: string): void {
this._noteMatrix.delete(unitId);
}
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import { DependentOn, IConfigService, Inject, Injector, Plugin, touchDependencies, UniverInstanceType } from '@univerjs/core';
import { UniverSheetsPlugin } from '@univerjs/sheets';
import { SheetsNoteRefRangeController } from './controllers/sheets-note-ref-range.controller';
import { SheetsNoteResourceController } from './controllers/sheets-note-resource.controller';
import { SheetsNoteController } from './controllers/sheets.note.controller';
import { SheetsNoteModel } from './models/sheets-note.model';
export const PLUGIN_NAME = 'SHEET_NOTE_PLUGIN';
@DependentOn(UniverSheetsPlugin)
export class SheetsNotePlugin extends Plugin {
static override pluginName = PLUGIN_NAME;
static override type = UniverInstanceType.UNIVER_SHEET;
constructor(
@Inject(Injector) protected override readonly _injector: Injector,
@IConfigService private readonly _configService: IConfigService
) {
super();
}
override onStarting(): void {
([
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
[SheetsNoteRefRangeController],
] as Dependency[]).forEach((dependency) => {
this._injector.add(dependency);
});
touchDependencies(this._injector, [
[SheetsNoteModel],
[SheetsNoteController],
[SheetsNoteResourceController],
]);
}
override onReady(): void {
touchDependencies(this._injector, [
[SheetsNoteRefRangeController],
]);
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/node",
"include": ["vitest.config.ts"]
}
+3
View File
@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();
+1 -1
View File
@@ -73,7 +73,7 @@
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-numfmt": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
+1 -1
View File
@@ -69,7 +69,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-sort": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
+1 -1
View File
@@ -75,7 +75,7 @@
"@univerjs/design": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
"@univerjs/sheets-sort": "workspace:*",
@@ -69,7 +69,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-thread-comment": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
@@ -17,7 +17,7 @@
import type { IDisposable, Nullable } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import { Disposable, DisposableCollection, Inject } from '@univerjs/core';
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
import { CellPopupManagerService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
import { IZenZoneService } from '@univerjs/ui';
import { BehaviorSubject } from 'rxjs';
import { SHEETS_THREAD_COMMENT_MODAL } from '../types/const';
@@ -42,7 +42,8 @@ export class SheetsThreadCommentPopupService extends Disposable {
constructor(
@Inject(SheetCanvasPopManagerService) private readonly _canvasPopupManagerService: SheetCanvasPopManagerService,
@IZenZoneService private readonly _zenZoneService: IZenZoneService
@IZenZoneService private readonly _zenZoneService: IZenZoneService,
@Inject(CellPopupManagerService) private readonly _cellPopupManagerService: CellPopupManagerService
) {
super();
this._initZenVisible();
@@ -83,9 +84,13 @@ export class SheetsThreadCommentPopupService extends Disposable {
this._activePopup = location;
this._activePopup$.next(location);
const popupDisposable = this._canvasPopupManagerService.attachPopupToCell(
row,
col,
const popupDisposable = this._cellPopupManagerService.showPopup(
{
row,
col,
unitId,
subUnitId,
},
{
componentKey: SHEETS_THREAD_COMMENT_MODAL,
onClickOutside: () => {
@@ -96,6 +101,7 @@ export class SheetsThreadCommentPopupService extends Disposable {
...Array.from(document.querySelectorAll('.univer-thread-comment')),
document.getElementById('thread-comment-add'),
].filter(Boolean) as HTMLElement[],
priority: 2,
}
);
+1 -1
View File
@@ -86,7 +86,7 @@
"@univerjs/engine-formula": "workspace:*",
"@univerjs/engine-numfmt": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/protocol": "0.1.45",
"@univerjs/sheets": "workspace:*",
"@univerjs/telemetry": "workspace:*",
@@ -104,6 +104,8 @@ import {
} from '../components/font-family';
import { FONT_SIZE_COMPONENT, FontSize } from '../components/font-size';
import { MENU_ITEM_INPUT_COMPONENT, MenuItemInput } from '../components/menu-item-input';
import { CellPopup } from '../views/cell-popup';
import { CELL_POPUP_COMPONENT_KEY } from '../views/cell-popup/config';
import { DEFINED_NAME_CONTAINER } from '../views/defined-name/component-name';
import { DefinedNameContainer } from '../views/defined-name/DefinedNameContainer';
import { RenderSheetContent, RenderSheetFooter, RenderSheetHeader } from '../views/sheet-container/SheetContainer';
@@ -198,6 +200,7 @@ export class SheetUIController extends Disposable {
this.disposeWithMe(componentManager.register(FONT_FAMILY_ITEM_COMPONENT, FontFamilyItem));
this.disposeWithMe(componentManager.register(FONT_SIZE_COMPONENT, FontSize));
this.disposeWithMe(componentManager.register(DEFINED_NAME_CONTAINER, DefinedNameContainer));
this.disposeWithMe(componentManager.register(CELL_POPUP_COMPONENT_KEY, CellPopup));
// init icons
this.disposeWithMe(componentManager.register('HideGridlines', HideGridlines));
+1
View File
@@ -99,6 +99,7 @@ export { useKeyEventConfig } from './views/editor-container';
export { type IDropdownParam, ISheetCellDropdownManagerService, SheetCellDropdownManagerService } from './services/cell-dropdown-manager.service';
export { FormulaBar } from './views/formula-bar/FormulaBar';
export { type IPermissionDetailUserPartProps } from './views/permission/panel-detail/PermissionDetailUserPart';
export { CellPopupManagerService } from './services/cell-popup-manager.service';
// #region - all commands
export { SetWorksheetColAutoWidthCommand } from './commands/commands/set-worksheet-auto-col-width.command';
export { AutoClearContentCommand, AutoFillCommand } from './commands/commands/auto-fill.command';
+2
View File
@@ -53,6 +53,7 @@ import { AutoFillService, IAutoFillService } from './services/auto-fill/auto-fil
import { SheetCanvasPopManagerService } from './services/canvas-pop-manager.service';
import { CellAlertManagerService } from './services/cell-alert-manager.service';
import { ISheetCellDropdownManagerService, SheetCellDropdownManagerService } from './services/cell-dropdown-manager.service';
import { CellPopupManagerService } from './services/cell-popup-manager.service';
import { ISheetClipboardService, SheetClipboardService } from './services/clipboard/clipboard.service';
import { DragManagerService } from './services/drag-manager.service';
import { EditorBridgeService, IEditorBridgeService } from './services/editor-bridge.service';
@@ -105,6 +106,7 @@ export class UniverSheetsMobileUIPlugin extends Plugin {
[HoverManagerService],
[DragManagerService],
[SheetCanvasPopManagerService],
[CellPopupManagerService],
[CellAlertManagerService],
[SelectAllService],
[ISheetCellDropdownManagerService, { useClass: SheetCellDropdownManagerService }],
+2
View File
@@ -68,6 +68,7 @@ import { AutoFillService, IAutoFillService } from './services/auto-fill/auto-fil
import { SheetCanvasPopManagerService } from './services/canvas-pop-manager.service';
import { CellAlertManagerService } from './services/cell-alert-manager.service';
import { ISheetCellDropdownManagerService, SheetCellDropdownManagerService } from './services/cell-dropdown-manager.service';
import { CellPopupManagerService } from './services/cell-popup-manager.service';
import { ISheetClipboardService, SheetClipboardService } from './services/clipboard/clipboard.service';
import { DragManagerService } from './services/drag-manager.service';
import { EditorBridgeService, IEditorBridgeService } from './services/editor-bridge.service';
@@ -156,6 +157,7 @@ export class UniverSheetsUIPlugin extends Plugin {
[HoverManagerService],
[DragManagerService],
[SheetCanvasPopManagerService],
[CellPopupManagerService],
[CellAlertManagerService],
[SelectAllService],
[ISheetCellDropdownManagerService, { useClass: SheetCellDropdownManagerService }],
@@ -18,11 +18,11 @@ import type { DrawingTypeEnum, ICommandInfo, INeedCheckDisposable, IRange, Nulla
import type { BaseObject, IBoundRectNoAngle, IRender, SpreadsheetSkeleton, Viewport } from '@univerjs/engine-render';
import type { ISetWorksheetRowAutoHeightMutationParams, ISheetLocationBase } from '@univerjs/sheets';
import type { IPopup } from '@univerjs/ui';
import { Disposable, DisposableCollection, ICommandService, Inject, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core';
import { Disposable, DisposableCollection, fromEventSubject, ICommandService, Inject, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { COMMAND_LISTENER_SKELETON_CHANGE, IRefSelectionsService, RefRangeService, SetFrozenMutation, SetWorksheetRowAutoHeightMutation, SheetsSelectionsService } from '@univerjs/sheets';
import { ICanvasPopupService } from '@univerjs/ui';
import { BehaviorSubject, map } from 'rxjs';
import { BehaviorSubject, map, throttleTime } from 'rxjs';
import { SetScrollOperation } from '../commands/operations/scroll.operation';
import { SetZoomRatioOperation } from '../commands/operations/set-zoom-ratio.operation';
import { getViewportByCell, transformBound2OffsetBound } from '../common/utils';
@@ -181,11 +181,14 @@ export class SheetCanvasPopManagerService extends Disposable {
disposable.add(this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetFrozenMutation.id) {
const newRects = calc();
rects$.next(newRects);
rects$.next(calc());
}
}));
disposable.add(fromEventSubject(currentRender.engine.onTransformChange$).pipe(throttleTime(16)).subscribe(() => {
rects$.next(calc());
}));
return {
rects$,
disposable,
@@ -597,6 +600,7 @@ export class SheetCanvasPopManagerService extends Disposable {
const disposable = new DisposableCollection();
disposable.add(currentRender.engine.clientRect$.subscribe(() => updatePosition()));
disposable.add(fromEventSubject(currentRender.engine.onTransformChange$).pipe(throttleTime(16)).subscribe(() => updatePosition()));
disposable.add(this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetWorksheetRowAutoHeightMutation.id) {
const params = commandInfo.params as ISetWorksheetRowAutoHeightMutationParams;
@@ -21,6 +21,7 @@ import { IRenderManagerService } from '@univerjs/engine-render';
import { Subject } from 'rxjs';
import { CELL_ALERT_KEY } from '../views/cell-alert';
import { SheetCanvasPopManagerService } from './canvas-pop-manager.service';
import { CellPopupManagerService } from './cell-popup-manager.service';
export enum CellAlertType {
INFO,
@@ -50,7 +51,8 @@ export class CellAlertManagerService extends Disposable {
constructor(
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@Inject(SheetCanvasPopManagerService) private readonly _canvasPopManagerService: SheetCanvasPopManagerService
@Inject(SheetCanvasPopManagerService) private readonly _canvasPopManagerService: SheetCanvasPopManagerService,
@Inject(CellPopupManagerService) private readonly _cellPopupManagerService: CellPopupManagerService
) {
super();
@@ -79,21 +81,29 @@ export class CellAlertManagerService extends Disposable {
}
const { location } = alert;
const { row, col, unitId } = location;
const { row, col, unitId, subUnitId } = location;
const currentRender = this._renderManagerService.getRenderById(unitId);
if (!currentRender) {
return;
}
const disposable = this._canvasPopManagerService.attachPopupToCell(row, col, {
componentKey: CELL_ALERT_KEY,
direction: 'horizontal',
offset: [2, 0],
extraProps: {
alert,
const disposable = this._cellPopupManagerService.showPopup(
{
unitId,
subUnitId,
row,
col,
},
});
{
componentKey: CELL_ALERT_KEY,
direction: 'horizontal',
extraProps: {
alert,
},
priority: 1,
}
);
if (disposable) {
lastPopup.dispose = disposable;
}
@@ -0,0 +1,144 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { IDisposable } from '@univerjs/core';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { ICanvasPopup } from './canvas-pop-manager.service';
import { Disposable, generateRandomId, Inject, ObjectMatrix, toDisposable } from '@univerjs/core';
import { Subject } from 'rxjs';
import { CELL_POPUP_COMPONENT_KEY } from '../views/cell-popup/config';
import { SheetCanvasPopManagerService } from './canvas-pop-manager.service';
export interface ICellPopup extends Omit<ICanvasPopup, 'direction' | 'offset' | 'mask'> {
direction?: 'horizontal' | 'vertical';
id?: string;
priority: number;
}
export interface ICellPopupDirectionCache {
popups: ICellPopup[];
disposable: IDisposable;
}
export interface ICellPopupCache {
horizontal?: ICellPopupDirectionCache;
vertical?: ICellPopupDirectionCache;
}
export interface ICellPopupChange extends ISheetLocationBase {
direction: 'horizontal' | 'vertical';
}
export class CellPopupManagerService extends Disposable {
private readonly _cellPopupMap = new Map<string, Map<string, ObjectMatrix<ICellPopupCache>>>();
private readonly _change$ = new Subject<ICellPopupChange>();
readonly change$ = this._change$.asObservable();
constructor(
@Inject(SheetCanvasPopManagerService) private readonly _sheetCanvasPopManagerService: SheetCanvasPopManagerService
) {
super();
}
private _ensureCellPopupMap(unitId: string, subUnitId: string) {
let unitMap = this._cellPopupMap.get(unitId);
if (!unitMap) {
unitMap = new Map();
this._cellPopupMap.set(unitId, unitMap);
}
let subUnitMap = unitMap.get(subUnitId);
if (!subUnitMap) {
subUnitMap = new ObjectMatrix<ICellPopupCache>();
unitMap.set(subUnitId, subUnitMap);
}
return subUnitMap;
}
showPopup(location: ISheetLocationBase, popup: ICellPopup) {
const { direction = 'horizontal' } = popup;
const { unitId, subUnitId, row, col } = location;
const subUnitMap = this._ensureCellPopupMap(unitId, subUnitId);
const directionCache = subUnitMap.getValue(row, col)?.[direction];
popup.id = popup.id ?? generateRandomId();
const hidePopup = () => {
const root = subUnitMap.getValue(row, col);
if (!root) {
return;
}
const directionCache = root[direction];
if (!directionCache) {
return;
}
const findIndex = directionCache?.popups.findIndex((p) => p.id === popup.id && p.componentKey === popup.componentKey);
directionCache?.popups.splice(findIndex, 1);
if (directionCache.popups.length === 0) {
directionCache.disposable.dispose();
root[direction] = undefined;
}
this._change$.next({
...location,
direction,
});
};
if (!directionCache) {
const disposable = this._sheetCanvasPopManagerService.attachPopupToCell(
row,
col,
{
...popup,
direction,
componentKey: CELL_POPUP_COMPONENT_KEY,
extraProps: {
...location,
direction,
},
noPushMinimumGap: true,
},
unitId,
subUnitId
);
subUnitMap.setValue(row, col, {
...subUnitMap.getValue(row, col),
[direction]: {
disposable,
popups: [popup],
},
});
} else {
directionCache.popups.push(popup);
directionCache.popups.sort((a, b) => a.priority - b.priority);
}
this._change$.next({
...location,
direction,
});
return toDisposable(hidePopup);
}
getPopups(unitId: string, subUnitId: string, row: number, col: number, direction: 'horizontal' | 'vertical') {
const subUnitMap = this._ensureCellPopupMap(unitId, subUnitId);
return subUnitMap.getValue(row, col)?.[direction]?.popups || [];
}
}
@@ -0,0 +1,17 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const CELL_POPUP_COMPONENT_KEY = 'sheets.component.cell-popup';
@@ -0,0 +1,42 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { IPopup } from '@univerjs/ui';
import { ComponentManager, useDependency, useObservable } from '@univerjs/ui';
import { useMemo } from 'react';
import { filter } from 'rxjs';
import { CellPopupManagerService } from '../../services/cell-popup-manager.service';
export const CellPopup = (props: { popup: IPopup<ISheetLocationBase & { direction: 'horizontal' | 'vertical' }> }) => {
const { popup } = props;
const location = popup.extraProps!;
const { row, col, direction, unitId, subUnitId } = location;
const cellPopupManagerService = useDependency(CellPopupManagerService);
useObservable(useMemo(() => cellPopupManagerService.change$.pipe(filter((change) => change.row === row && change.col === col && change.direction === direction)), [cellPopupManagerService, row, col, direction]));
const popups = cellPopupManagerService.getPopups(unitId, subUnitId, row, col, direction);
const componentManager = useDependency(ComponentManager);
return (
<div className="univer-cell-popup univer-flex univer-flex-col univer-ml-[1px]">
{popups.map((item) => {
const popup = item;
const Component = componentManager.get(popup.componentKey);
return Component ? <Component popup={popup} /> : null;
})}
</div>
);
};
+1 -1
View File
@@ -84,7 +84,7 @@
"@univerjs/docs": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
+1 -1
View File
@@ -74,7 +74,7 @@
"@univerjs/docs-ui": "workspace:*",
"@univerjs/drawing": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/slides": "workspace:*",
"@univerjs/ui": "workspace:*"
},
+1 -1
View File
@@ -69,7 +69,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/docs-ui": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@univerjs/thread-comment": "workspace:*",
"@univerjs/ui": "workspace:*"
},
+1 -1
View File
@@ -83,7 +83,7 @@
"@univerjs/core": "workspace:*",
"@univerjs/design": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.3.22",
"@univerjs/icons": "^0.3.24",
"@wendellhu/redi": "0.18.0",
"localforage": "^1.10.0"
},
@@ -18,4 +18,5 @@ export { useEvent } from './event';
export { useConfigValue } from './layout';
export { useUpdateEffect } from './update-effect';
export { useClickOutSide } from './use-click-out-side';
export { useDebounceFn } from './use-debounce';
export { useVirtualList } from './virtual-list';
@@ -0,0 +1,31 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useRef } from 'react';
export function useDebounceFn<T extends (...args: any[]) => void>(fn: T, delay = 300) {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback((...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
fn(...args);
}, delay);
}, [fn, delay]) as T;
}
+1 -1
View File
@@ -127,7 +127,7 @@ export { MobileContextMenu } from './views/components/context-menu/MobileContext
export { type IDialogPartMethodOptions } from './views/components/dialog-part/interface';
export { FloatDom } from './views/components/dom/FloatDom';
export { GlobalZone } from './views/components/global-zone/GlobalZone';
export { CanvasPopup } from './views/components/popup/CanvasPopup';
export { CanvasPopup, SingleCanvasPopup } from './views/components/popup/CanvasPopup';
export { ToolbarButton } from './views/components/ribbon/Button/ToolbarButton';
export { useToolbarItemStatus } from './views/components/ribbon/hook';
export { Ribbon } from './views/components/ribbon/Ribbon';
@@ -27,7 +27,7 @@ interface ISingleCanvasPopupProps {
children?: React.ReactNode;
}
const SingleCanvasPopup = ({ popup, children }: ISingleCanvasPopupProps) => {
export const SingleCanvasPopup = ({ popup, children }: ISingleCanvasPopupProps) => {
const [hidden, setHidden] = useState(false);
const anchorRect$ = useMemo(() => popup.anchorRect$.pipe(
throttleTime(0, animationFrameScheduler),
@@ -85,6 +85,7 @@ export interface IRectPopupProps {
zIndex?: number;
maskZIndex?: number;
onMaskClick?: () => void;
noPushMinimumGap?: boolean;
}
export interface IPopupLayoutInfo extends Pick<IRectPopupProps, 'direction'> {
@@ -93,33 +94,36 @@ export interface IPopupLayoutInfo extends Pick<IRectPopupProps, 'direction'> {
height: number;
containerWidth: number;
containerHeight: number;
noPushMinimumGap?: boolean;
}
/** The popup should have a minimum edge to the boundary. */
const PUSHING_MINIMUM_GAP = 8;
function calcPopupPosition(layout: IPopupLayoutInfo): { top: number; left: number } {
const { position, width, height, containerHeight, containerWidth, direction = 'vertical' } = layout;
const { position, width, height, containerHeight, containerWidth, direction = 'vertical', noPushMinimumGap = false } = layout;
const minTop = noPushMinimumGap ? -Infinity : PUSHING_MINIMUM_GAP;
const maxTop = noPushMinimumGap ? Infinity : containerHeight - height - PUSHING_MINIMUM_GAP;
const minLeft = noPushMinimumGap ? -Infinity : PUSHING_MINIMUM_GAP;
const maxLeft = noPushMinimumGap ? Infinity : containerWidth - width - PUSHING_MINIMUM_GAP;
// In y-axis
if (direction === 'vertical' || direction.indexOf('top') === 0 || direction.indexOf('bottom') === 0) {
const { left: startX, top: startY, right: endX, bottom: endY } = position;
const verticalStyle = (direction === 'vertical' && endY > containerHeight - height - PUSHING_MINIMUM_GAP) || direction.indexOf('top') > -1
const verticalStyle = (direction === 'vertical' && endY > maxTop) || direction.indexOf('top') > -1
// top
? { top: Math.max(startY - height, PUSHING_MINIMUM_GAP) }
? { top: Math.max(startY - height, minTop) }
// bottom
: { top: Math.min(endY, containerHeight - height - PUSHING_MINIMUM_GAP) };
: { top: Math.min(endY, maxTop) };
let horizontalStyle;
const minLeft = PUSHING_MINIMUM_GAP;
const maxLeft = containerWidth - width - PUSHING_MINIMUM_GAP;
if (direction.includes('center')) {
const rectWidth = endX - startX;
const offsetX = (rectWidth - width) / 2;
horizontalStyle = (Math.max(startX + offsetX, PUSHING_MINIMUM_GAP) + width) > containerWidth
horizontalStyle = (Math.max(startX + offsetX, minLeft) + width) > containerWidth
? { left: Math.max(Math.min(maxLeft, endX - width - offsetX), minLeft) }
: { left: Math.max(minLeft, Math.min(startX + offsetX, maxLeft)) };
} else if (direction.includes('left')) {
@@ -140,33 +144,31 @@ function calcPopupPosition(layout: IPopupLayoutInfo): { top: number; left: numbe
const { left: startX, top: startY, right: endX, bottom: endY } = position;
// const horizontalStyle = ((endX + width) > containerWidth || direction === 'left')
const horizontalStyle = direction.includes('left')
? { left: Math.max(startX - width, PUSHING_MINIMUM_GAP) } // on left
: { left: Math.min(endX, containerWidth - width - PUSHING_MINIMUM_GAP) }; // on right
? { left: Math.max(startX - width, minLeft) } // on left
: { left: Math.min(endX, maxLeft) }; // on right
let verticalStyle;
const minTop = PUSHING_MINIMUM_GAP;
const maxTop = containerHeight - height - PUSHING_MINIMUM_GAP;
if (direction.includes('center')) {
const rectHeight = endY - startY;
const offsetY = (rectHeight - height) / 2;
verticalStyle = (Math.max(startY + offsetY, PUSHING_MINIMUM_GAP) + height) > containerHeight
verticalStyle = (Math.max(startY + offsetY, minTop) + height) > containerHeight
? { top: Math.max(Math.min(maxTop, endY - height - offsetY), minTop) }
: { top: Math.max(minTop, Math.min(startY + offsetY, maxTop)) };
} else if (direction.includes('top')) {
verticalStyle = {
top: Math.min(startY, containerHeight - height - PUSHING_MINIMUM_GAP),
top: Math.min(startY, maxTop),
};
} else if (direction.includes('bottom')) {
verticalStyle = {
top: Math.max(endY - height, PUSHING_MINIMUM_GAP),
top: Math.max(endY - height, minTop),
};
} else {
// If the popup element exceed the visible area. We should "push" it back.
verticalStyle = ((startY + height) > containerHeight)
? { top: Math.max(endY - height, PUSHING_MINIMUM_GAP) } // on top
: { top: Math.min(startY, containerHeight - height - PUSHING_MINIMUM_GAP) }; // on bottom
? { top: Math.max(endY - height, minTop) } // on top
: { top: Math.min(startY, maxTop) }; // on bottom
}
return { ...verticalStyle, ...horizontalStyle };
@@ -190,6 +192,7 @@ function RectPopup(props: IRectPopupProps) {
zIndex = 1020,
maskZIndex = 100,
onMaskClick,
noPushMinimumGap,
} = props;
const nodeRef = useRef<HTMLElement>(null);
const clickOtherFn = useEvent(onClickOutside ?? (() => { /* empty */ }));
@@ -220,6 +223,7 @@ function RectPopup(props: IRectPopupProps) {
containerWidth: innerWidth,
containerHeight: innerHeight,
direction,
noPushMinimumGap,
}
);
+974 -68
View File
File diff suppressed because it is too large Load Diff