mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
feat: uni mode (#2827)
Co-authored-by: weird94 <zhang9748@foxmail.com> Co-authored-by: Dushusir <1414556676@qq.com> Co-authored-by: YuHong <csuzyh@gmail.com> Co-authored-by: 白熱 <sonne@asaki.me> Co-authored-by: siam <siam1103052133@gmail.com>
This commit is contained in:
co-authored by
weird94
Dushusir
YuHong
白熱
siam
parent
b59624e569
commit
36ac2bf2be
Vendored
+3
-1
@@ -101,6 +101,7 @@
|
||||
"Ungroup",
|
||||
"unhine",
|
||||
"Uniscript",
|
||||
"Uniui",
|
||||
"univer",
|
||||
"univerdoc",
|
||||
"univerjs",
|
||||
@@ -169,5 +170,6 @@
|
||||
"toml",
|
||||
"less",
|
||||
"css"
|
||||
]
|
||||
],
|
||||
"commentTranslate.multiLineMerge": true
|
||||
}
|
||||
|
||||
@@ -16,21 +16,6 @@
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// The type definition is copied from:
|
||||
// examples/src/plugins/debugger/controllers/e2e/e2e-memory.controller.ts
|
||||
export interface IE2EControllerAPI {
|
||||
loadAndRelease(id: number): Promise<void>;
|
||||
loadDefaultSheet(): Promise<void>;
|
||||
disposeUniver(): Promise<void>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface Window {
|
||||
E2EControllerAPI: IE2EControllerAPI;
|
||||
}
|
||||
}
|
||||
|
||||
test('no error on constructing and disposing', async ({ page }) => {
|
||||
let errored = false;
|
||||
|
||||
|
||||
@@ -56,7 +56,11 @@
|
||||
"@univerjs/thread-comment": "workspace:*",
|
||||
"@univerjs/thread-comment-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uni-docs-ui": "workspace:*",
|
||||
"@univerjs/uni-formula": "workspace:*",
|
||||
"@univerjs/uni-formula-ui": "workspace:*",
|
||||
"@univerjs/uni-sheets-ui": "workspace:*",
|
||||
"@univerjs/uni-slides-ui": "workspace:*",
|
||||
"@univerjs/uniscript": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { ptToPixel } from '@univerjs/engine-render';
|
||||
|
||||
export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = {
|
||||
id: 'd',
|
||||
id: 'd-en',
|
||||
drawings: {
|
||||
shapeTest1: {
|
||||
unitId: 'd',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,7 +30,7 @@ import { UniverDocsMentionUIPlugin } from '@univerjs/docs-mention-ui';
|
||||
import { DEFAULT_DOCUMENT_DATA_CN } from '../data';
|
||||
import { enUS, ruRU, zhCN } from '../locales';
|
||||
|
||||
// const IS_E2E: boolean = !!process.env.IS_E2E;
|
||||
const IS_E2E: boolean = !!process.env.IS_E2E;
|
||||
|
||||
// package info
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -77,7 +77,9 @@ univer.registerPlugin(UniverDocsThreadCommentUIPlugin);
|
||||
univer.registerPlugin(UniverDocsHyperLinkUIPlugin);
|
||||
univer.registerPlugin(UniverDocsMentionUIPlugin);
|
||||
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_CN);
|
||||
if (!IS_E2E) {
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_CN);
|
||||
}
|
||||
|
||||
// use for console test
|
||||
declare global {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import type { Plugin, PluginCtor } from '@univerjs/core';
|
||||
import { UniverSheetsFilterUIPlugin } from '@univerjs/sheets-filter-ui';
|
||||
import { UniverDocUniFormulaUIPlugin } from '@univerjs/uni-formula-ui';
|
||||
// import { UniverUniscriptPlugin } from '@univerjs/uniscript';
|
||||
|
||||
export default function getLazyPlugins(): Array<[PluginCtor<Plugin>] | [PluginCtor<Plugin>, unknown]> {
|
||||
@@ -33,5 +34,6 @@ export default function getLazyPlugins(): Array<[PluginCtor<Plugin>] | [PluginCt
|
||||
// },
|
||||
// ],
|
||||
[UniverSheetsFilterUIPlugin],
|
||||
[UniverDocUniFormulaUIPlugin],
|
||||
];
|
||||
}
|
||||
|
||||
+89
-49
@@ -15,20 +15,20 @@
|
||||
*/
|
||||
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import { LocaleType, LogLevel, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { Injector, LocaleType, LogLevel, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { defaultTheme } from '@univerjs/design';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { DocUIController, UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
||||
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { SheetUIController, UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { UniSheetsUIController } from '@univerjs/uni-sheets-ui';
|
||||
import { UniverUniUIPlugin } from '@univerjs/uniui';
|
||||
import { UniverDebuggerPlugin } from '@univerjs/debugger';
|
||||
import { FUniver } from '@univerjs/facade';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverSheetsConditionalFormattingUIPlugin } from '@univerjs/sheets-conditional-formatting-ui';
|
||||
import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui';
|
||||
import { UniverSheetsHyperLinkUIPlugin } from '@univerjs/sheets-hyper-link-ui';
|
||||
@@ -40,15 +40,18 @@ import { UniverSheetsDrawingUIPlugin } from '@univerjs/sheets-drawing-ui';
|
||||
import type { IUniverRPCMainThreadConfig } from '@univerjs/rpc';
|
||||
import { UniverRPCMainThreadPlugin } from '@univerjs/rpc';
|
||||
import { UniverDocUniFormulaPlugin } from '@univerjs/uni-formula';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATA_CN } from '../data';
|
||||
import { UniverDocsDrawingUIPlugin } from '@univerjs/docs-drawing-ui';
|
||||
import { UniSlidesUIController } from '@univerjs/uni-slides-ui';
|
||||
import { UniverSlidesPlugin } from '@univerjs/slides';
|
||||
import { SlidesUIController, UniverSlidesUIPlugin } from '@univerjs/slides-ui';
|
||||
import { UniDocsUIController } from '@univerjs/uni-docs-ui';
|
||||
import { DEFAULT_DOCUMENT_DATA_CN, DEFAULT_DOCUMENT_DATA_EN, DEFAULT_SLIDE_DATA, DEFAULT_WORKBOOK_DATA_DEMO, DEFAULT_WORKBOOK_DATA_DEMO1 } from '../data';
|
||||
import { enUS } from '../locales';
|
||||
import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo';
|
||||
|
||||
/* eslint-disable-next-line node/prefer-global/process */
|
||||
const IS_E2E: boolean = !!process.env.IS_E2E;
|
||||
|
||||
const LOAD_LAZY_PLUGINS_TIMEOUT = 1_000;
|
||||
const LOAD_LAZY_PLUGINS_TIMEOUT = 4_000;
|
||||
|
||||
// univer
|
||||
const univer = new Univer({
|
||||
@@ -60,36 +63,10 @@ const univer = new Univer({
|
||||
logLevel: LogLevel.VERBOSE,
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverDocsPlugin, {
|
||||
hasScroll: false,
|
||||
});
|
||||
univer.registerPlugin(UniverFormulaEnginePlugin, {
|
||||
notExecuteFormula: true,
|
||||
});
|
||||
univer.registerPlugin(UniverRenderEnginePlugin);
|
||||
univer.registerPlugin(UniverUniUIPlugin, {
|
||||
container: 'app',
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverDrawingPlugin);
|
||||
|
||||
univer.registerPlugin(UniverDocsUIPlugin);
|
||||
|
||||
univer.registerPlugin(UniverSheetsPlugin);
|
||||
univer.registerPlugin(UniverSheetsUIPlugin);
|
||||
|
||||
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
||||
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
||||
univer.registerPlugin(UniverRPCMainThreadPlugin, {
|
||||
workerURL: './worker.js',
|
||||
} as IUniverRPCMainThreadConfig);
|
||||
|
||||
univer.registerPlugin(UniverSheetsFindReplacePlugin);
|
||||
univer.registerPlugin(UniverSheetsHyperLinkUIPlugin);
|
||||
// univer.registerPlugin(UniverSheetsDataValidationPlugin);
|
||||
univer.registerPlugin(UniverSheetsSortUIPlugin);
|
||||
|
||||
univer.registerPlugin(UniverDocUniFormulaPlugin);
|
||||
registerBasicPlugins(univer);
|
||||
registerSheetPlugins(univer);
|
||||
registerSlidePlugins(univer);
|
||||
registerUniPlugins(univer);
|
||||
|
||||
const mockUser = {
|
||||
userID: 'Owner_qxVnhPbQ',
|
||||
@@ -121,19 +98,11 @@ class CustomMentionDataService implements IThreadCommentMentionDataService {
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(UniverThreadCommentUIPlugin, {
|
||||
overrides: [[IThreadCommentMentionDataService, { useClass: CustomMentionDataService }]],
|
||||
});
|
||||
univer.registerPlugin(UniverSheetsThreadCommentPlugin);
|
||||
univer.registerPlugin(UniverSheetsConditionalFormattingUIPlugin);
|
||||
|
||||
univer.registerPlugin(UniverSheetsDrawingUIPlugin);
|
||||
|
||||
// create univer sheet instance
|
||||
// create univer instances
|
||||
if (!IS_E2E) {
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO);
|
||||
// univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO1);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_CN);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_EN);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO1);
|
||||
}
|
||||
|
||||
// debugger plugin
|
||||
@@ -151,8 +120,79 @@ setTimeout(() => {
|
||||
import('./lazy').then((lazy) => {
|
||||
const plugins = lazy.default();
|
||||
plugins.forEach((p) => univer.registerPlugin(p[0], p[1]));
|
||||
|
||||
univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_CN);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SLIDE, DEFAULT_SLIDE_DATA);
|
||||
});
|
||||
}, LOAD_LAZY_PLUGINS_TIMEOUT);
|
||||
|
||||
window.univer = univer;
|
||||
window.univerAPI = FUniver.newAPI(univer);
|
||||
|
||||
function registerBasicPlugins(univer: Univer) {
|
||||
univer.registerPlugin(UniverDocsPlugin, {
|
||||
hasScroll: false,
|
||||
});
|
||||
univer.registerPlugin(UniverFormulaEnginePlugin, {
|
||||
notExecuteFormula: true,
|
||||
});
|
||||
univer.registerPlugin(UniverRenderEnginePlugin);
|
||||
univer.registerPlugin(UniverUniUIPlugin, {
|
||||
container: 'app',
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverDocsUIPlugin, {
|
||||
override: [
|
||||
[DocUIController, {
|
||||
useFactory: (injector) => {
|
||||
injector.createInstance(UniDocsUIController, {});
|
||||
}, deps: [Injector],
|
||||
}],
|
||||
],
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverRPCMainThreadPlugin, {
|
||||
workerURL: './worker.js',
|
||||
} as IUniverRPCMainThreadConfig);
|
||||
univer.registerPlugin(UniverThreadCommentUIPlugin, {
|
||||
overrides: [[IThreadCommentMentionDataService, { useClass: CustomMentionDataService }]],
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverDocsDrawingUIPlugin);
|
||||
}
|
||||
|
||||
function registerSheetPlugins(univer: Univer) {
|
||||
univer.registerPlugin(UniverSheetsPlugin);
|
||||
univer.registerPlugin(UniverSheetsUIPlugin, {
|
||||
override: [
|
||||
[SheetUIController, {
|
||||
useFactory: (injector) => {
|
||||
injector.createInstance(UniSheetsUIController, {});
|
||||
}, deps: [Injector],
|
||||
}],
|
||||
],
|
||||
});
|
||||
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
||||
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
||||
univer.registerPlugin(UniverSheetsFindReplacePlugin);
|
||||
univer.registerPlugin(UniverSheetsHyperLinkUIPlugin);
|
||||
univer.registerPlugin(UniverSheetsSortUIPlugin);
|
||||
univer.registerPlugin(UniverSheetsThreadCommentPlugin);
|
||||
univer.registerPlugin(UniverSheetsConditionalFormattingUIPlugin);
|
||||
univer.registerPlugin(UniverSheetsDrawingUIPlugin);
|
||||
}
|
||||
|
||||
function registerSlidePlugins(univer: Univer) {
|
||||
univer.registerPlugin(UniverSlidesPlugin);
|
||||
univer.registerPlugin(UniverSlidesUIPlugin, {
|
||||
override: [
|
||||
[SlidesUIController, {
|
||||
useClass: UniSlidesUIController,
|
||||
}],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function registerUniPlugins(univer: Univer) {
|
||||
univer.registerPlugin(UniverDocUniFormulaPlugin);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocaleType, LogLevel, Univer } from '@univerjs/core';
|
||||
import { LocaleType, Univer } from '@univerjs/core';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { UniverRPCWorkerThreadPlugin } from '@univerjs/rpc';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
|
||||
const univer = new Univer({
|
||||
locale: LocaleType.ZH_CN,
|
||||
logLevel: LogLevel.VERBOSE,
|
||||
});
|
||||
|
||||
univer.registerPlugin(UniverSheetsPlugin, { onlyRegisterFormulaRelatedMutations: true });
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# @univerjs/uni-docs-ui
|
||||
|
||||
[](https://npmjs.org/packages/@univerjs/uni-sheets-ui)
|
||||
[](https://img.shields.io/npm/l/@univerjs/uni-sheets-ui)
|
||||
|
||||
## Introduction
|
||||
|
||||
> TODO: Introduction
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```shell
|
||||
npm i @univerjs/uni-docs-ui
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "@univerjs/uni-docs-ui",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"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/*"
|
||||
},
|
||||
"main": "./lib/cjs/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/cjs/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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-drawing-ui": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/icons": "^0.1.72"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/shared": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"univerSpace": {
|
||||
".": {
|
||||
"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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { BooleanNumber, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { SetInlineFormatCommand, SetTextSelectionsOperation, TextSelectionManagerService } from '@univerjs/docs';
|
||||
|
||||
import { getMenuHiddenObservable, type IMenuButtonItem, MenuGroup, MenuItemType } from '@univerjs/ui';
|
||||
import { FONT_GROUP_MENU_ID } from '@univerjs/uniui';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export const DOC_ITALIC_MUTATION_ID = 'doc.command.uni-italic';
|
||||
export const DOC_BOLD_MUTATION_ID = 'doc.command.uni-bold';
|
||||
export const DOC_UNDERLINE_MUTATION_ID = 'doc.command.uni-underline';
|
||||
export const DOC_STRIKE_MUTATION_ID = 'doc.command.uni-strike';
|
||||
|
||||
export function DocBoldMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return {
|
||||
id: DOC_BOLD_MUTATION_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'BoldSingle',
|
||||
title: 'Set bold',
|
||||
tooltip: 'toolbar.bold',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
activated$: new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
|
||||
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
|
||||
if (textRun == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bl = textRun.ts?.bl;
|
||||
|
||||
subscriber.next(bl === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
subscriber.next(false);
|
||||
|
||||
return disposable.dispose;
|
||||
}),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function DocItalicMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return {
|
||||
id: DOC_ITALIC_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'ItalicSingle',
|
||||
title: 'Set italic',
|
||||
tooltip: 'toolbar.italic',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
activated$: new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
|
||||
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
|
||||
if (textRun == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const it = textRun.ts?.it;
|
||||
|
||||
subscriber.next(it === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
subscriber.next(false);
|
||||
|
||||
return disposable.dispose;
|
||||
}),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function DocUnderlineMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return {
|
||||
id: DOC_UNDERLINE_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'UnderlineSingle',
|
||||
title: 'Set underline',
|
||||
tooltip: 'toolbar.underline',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
activated$: new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
|
||||
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
|
||||
if (textRun == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ul = textRun.ts?.ul;
|
||||
|
||||
subscriber.next(ul?.s === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
subscriber.next(false);
|
||||
|
||||
return disposable.dispose;
|
||||
}),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function DocStrikeThroughMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return {
|
||||
id: DOC_STRIKE_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'StrikethroughSingle',
|
||||
title: 'Set strike through',
|
||||
tooltip: 'toolbar.strikethrough',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
activated$: new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
|
||||
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
|
||||
if (textRun == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const st = textRun.ts?.st;
|
||||
|
||||
subscriber.next(st?.s === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
subscriber.next(false);
|
||||
|
||||
return disposable.dispose;
|
||||
}),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
function getFontStyleAtCursor(accessor: IAccessor) {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const textSelectionService = accessor.get(TextSelectionManagerService);
|
||||
|
||||
const editorDataModel = univerInstanceService.getUniverDocInstance(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
const activeTextRange = textSelectionService.getActiveTextRange();
|
||||
|
||||
if (editorDataModel == null || activeTextRange == null) return null;
|
||||
|
||||
const textRuns = editorDataModel.getBody()?.textRuns;
|
||||
if (textRuns == null) return;
|
||||
|
||||
const { startOffset } = activeTextRange;
|
||||
const textRun = textRuns.find(({ st, ed }) => startOffset! >= st && startOffset! <= ed);
|
||||
return textRun;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { ICommandService, Inject, Injector, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core';
|
||||
import type { IMenuItemFactory } from '@univerjs/ui';
|
||||
import { ComponentManager, ILayoutService, IMenuService, IShortcutService, IUIPartsService } from '@univerjs/ui';
|
||||
import { BuiltinUniToolbarItemId, generateCloneMutation, UniToolbarService } from '@univerjs/uniui';
|
||||
import type { IUniverDocsUIConfig } from '@univerjs/docs-ui';
|
||||
import { DocUIController } from '@univerjs/docs-ui';
|
||||
import { SetInlineFormatBoldCommand, SetInlineFormatFontFamilyCommand, SetInlineFormatFontSizeCommand, SetInlineFormatItalicCommand, SetInlineFormatStrikethroughCommand, SetInlineFormatTextBackgroundColorCommand, SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand } from '@univerjs/docs';
|
||||
import { IMAGE_MENU_ID as DocsImageMenuId } from '@univerjs/docs-drawing-ui';
|
||||
import { DOC_BOLD_MUTATION_ID, DOC_ITALIC_MUTATION_ID, DOC_STRIKE_MUTATION_ID, DOC_UNDERLINE_MUTATION_ID, DocBoldMenuItemFactory, DocItalicMenuItemFactory, DocStrikeThroughMenuItemFactory, DocUnderlineMenuItemFactory } from './menu';
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, UniDocsUIController)
|
||||
export class UniDocsUIController extends DocUIController {
|
||||
constructor(
|
||||
config: Partial<IUniverDocsUIConfig>,
|
||||
@Inject(Injector) injector: Injector,
|
||||
@Inject(ComponentManager) componentManager: ComponentManager,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@ILayoutService layoutService: ILayoutService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IUIPartsService uiPartsService: IUIPartsService,
|
||||
@IUniverInstanceService univerInstanceService: IUniverInstanceService,
|
||||
@IShortcutService shortcutService: IShortcutService,
|
||||
@Inject(UniToolbarService) private readonly _toolbarService: UniToolbarService
|
||||
|
||||
) {
|
||||
super(
|
||||
config,
|
||||
injector,
|
||||
componentManager,
|
||||
commandService,
|
||||
layoutService,
|
||||
menuService,
|
||||
uiPartsService,
|
||||
univerInstanceService,
|
||||
shortcutService
|
||||
);
|
||||
this._initUniMenus();
|
||||
this._initMutations();
|
||||
}
|
||||
|
||||
private _initUniMenus(): void {
|
||||
([
|
||||
[BuiltinUniToolbarItemId.FONT_FAMILY, SetInlineFormatFontFamilyCommand.id],
|
||||
[BuiltinUniToolbarItemId.FONT_SIZE, SetInlineFormatFontSizeCommand.id],
|
||||
[BuiltinUniToolbarItemId.COLOR, SetInlineFormatTextColorCommand.id],
|
||||
[BuiltinUniToolbarItemId.BACKGROUND, SetInlineFormatTextBackgroundColorCommand.id],
|
||||
[BuiltinUniToolbarItemId.IMAGE, DocsImageMenuId],
|
||||
]).forEach(([id, menuId]) => {
|
||||
this.disposeWithMe(this._toolbarService.implementItem(id, { id: menuId, type: UniverInstanceType.UNIVER_DOC }));
|
||||
});
|
||||
|
||||
(
|
||||
[
|
||||
DocBoldMenuItemFactory,
|
||||
DocItalicMenuItemFactory,
|
||||
DocUnderlineMenuItemFactory,
|
||||
DocStrikeThroughMenuItemFactory,
|
||||
] as IMenuItemFactory[]
|
||||
).forEach((factory) => {
|
||||
this.disposeWithMe(this._menuService.addMenuItem(this._injector.invoke(factory), {}));
|
||||
});
|
||||
}
|
||||
|
||||
private _initMutations() {
|
||||
[
|
||||
generateCloneMutation(DOC_BOLD_MUTATION_ID, SetInlineFormatBoldCommand),
|
||||
generateCloneMutation(DOC_ITALIC_MUTATION_ID, SetInlineFormatItalicCommand),
|
||||
generateCloneMutation(DOC_UNDERLINE_MUTATION_ID, SetInlineFormatUnderlineCommand),
|
||||
generateCloneMutation(DOC_STRIKE_MUTATION_ID, SetInlineFormatStrikethroughCommand),
|
||||
].forEach((mutation) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(mutation));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { UniDocsUIController } from './controllers/uni-docs-ui.controller';
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@univerjs/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@univerjs/shared/tsconfigs/node",
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import createViteConfig from '@univerjs/shared/vite';
|
||||
import pkg from './package.json';
|
||||
|
||||
export default ({ mode }) => createViteConfig({}, {
|
||||
mode,
|
||||
pkg,
|
||||
features: {
|
||||
react: false,
|
||||
css: true,
|
||||
dom: true,
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@univerjs/uni-formula-ui",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"author": "DreamNum <developer@univer.ai>",
|
||||
@@ -20,7 +20,8 @@
|
||||
"keywords": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*"
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.ts"
|
||||
},
|
||||
"main": "./lib/cjs/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
@@ -40,7 +41,8 @@
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
"./lib/*": "./lib/*",
|
||||
"./locale/*": "./lib/locale/*.json"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
@@ -58,12 +60,27 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/uni-formula": "workspace:*"
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/icons": "^0.1.72",
|
||||
"@univerjs/sheets-formula": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uni-formula": "workspace:*",
|
||||
"clsx": ">=2.0.0",
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/icons": "^0.1.72",
|
||||
"@univerjs/shared": "workspace:*",
|
||||
"@univerjs/sheets-formula": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uni-formula": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5",
|
||||
@@ -80,6 +97,7 @@
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
"./lib/*": "./lib/*",
|
||||
"./locale/*": "./lib/locale/*.json"
|
||||
}
|
||||
}
|
||||
|
||||
+29
-11
@@ -14,27 +14,45 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandType, CustomRangeType, generateRandomId, type ICommand, ICommandService, type IMutationInfo, sequenceExecute } from '@univerjs/core';
|
||||
import { addCustomRangeBySelectionFactory } from '@univerjs/docs';
|
||||
|
||||
import type { IAddDocUniFormulaMutationParams, IRemoveDocUniFormulaMutationParams, IUpdateDocUniFormulaMutationParams } from './mutation';
|
||||
import { AddDocUniFormulaMutation, RemoveDocUniFormulaMutation, UpdateDocUniFormulaMutation } from './mutation';
|
||||
import type { ICommand, IDocumentBody, IMutationInfo } from '@univerjs/core';
|
||||
import { CommandType, CustomRangeType, generateRandomId, ICommandService, LocaleService, makeCustomRangeStream, sequenceExecute } from '@univerjs/core';
|
||||
import { makeSelection, replaceSelectionFactory } from '@univerjs/docs';
|
||||
import type { IAddDocUniFormulaMutationParams, IRemoveDocUniFormulaMutationParams, IUpdateDocUniFormulaMutationParams } from '@univerjs/uni-formula';
|
||||
import { AddDocUniFormulaMutation, RemoveDocUniFormulaMutation, UpdateDocUniFormulaMutation } from '@univerjs/uni-formula';
|
||||
|
||||
export interface IAddDocUniFormulaCommandParams {
|
||||
unitId: string;
|
||||
f: string;
|
||||
startIndex: number;
|
||||
}
|
||||
|
||||
export const AddDocUniFormulaCommand: ICommand<IAddDocUniFormulaCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'doc.command.add-uni-formula',
|
||||
async handler(accessor, params: IAddDocUniFormulaCommandParams) {
|
||||
const { f, unitId } = params;
|
||||
const { f, unitId, startIndex } = params;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
|
||||
const rangeId = generateRandomId();
|
||||
const redoMutation = addCustomRangeBySelectionFactory(accessor, {
|
||||
rangeId,
|
||||
rangeType: CustomRangeType.CUSTOM,
|
||||
const placeholder = localeService.t('uni-formula.command.stream-placeholder');
|
||||
const dataStream = makeCustomRangeStream(placeholder);
|
||||
const body: IDocumentBody = {
|
||||
dataStream,
|
||||
customRanges: [{
|
||||
startIndex: 0,
|
||||
endIndex: dataStream.length - 1,
|
||||
rangeId,
|
||||
rangeType: CustomRangeType.UNI_FORMULA,
|
||||
wholeEntity: true,
|
||||
}],
|
||||
};
|
||||
|
||||
const redoMutation = replaceSelectionFactory(accessor, {
|
||||
unitId,
|
||||
body,
|
||||
selection: makeSelection(startIndex, startIndex + 1),
|
||||
});
|
||||
|
||||
if (redoMutation) {
|
||||
@@ -60,7 +78,7 @@ export interface IUpdateDocUniFormulaCommandParams {
|
||||
|
||||
export const UpdateDocUniFormulaCommand: ICommand<IUpdateDocUniFormulaCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'doc.command.add-uni-formula',
|
||||
id: 'doc.command.update-uni-formula',
|
||||
handler: (accessor, params: IUpdateDocUniFormulaCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(UpdateDocUniFormulaMutation.id, params as IUpdateDocUniFormulaMutationParams);
|
||||
@@ -77,7 +95,7 @@ export const RemoveDocUniFormulaCommand: ICommand<IRemoveDocUniFormulaCommandPar
|
||||
id: 'doc.command.remove-uni-formula',
|
||||
handler: (accessor, params: IRemoveDocUniFormulaCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
// TODO: maybed remove formula under selection?
|
||||
// TODO: maybe remove formula under selection?
|
||||
return commandService.syncExecuteCommand(RemoveDocUniFormulaMutation.id, params as IRemoveDocUniFormulaMutationParams);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, IOperation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { DocFormulaPopupService } from '../services/formula-popup.service';
|
||||
|
||||
export interface IShowFormulaPopupOperationParams {
|
||||
unitId: string;
|
||||
startIndex: number;
|
||||
|
||||
type?: 'new' | 'existing';
|
||||
rangeId?: string;
|
||||
}
|
||||
|
||||
export const ShowFormulaPopupOperation: IOperation<IShowFormulaPopupOperationParams> = {
|
||||
id: 'doc.operation.show-formula-popup',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor, params: IShowFormulaPopupOperationParams) {
|
||||
const { type = 'new', startIndex, unitId, rangeId } = params;
|
||||
const docFormulaPopupService = accessor.get(DocFormulaPopupService);
|
||||
|
||||
if (type === 'existing' && !rangeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return docFormulaPopupService.showPopup(unitId, startIndex, type, rangeId);
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseFormulaPopupOperation: IOperation = {
|
||||
id: 'doc.operation.close-formula-popup',
|
||||
type: CommandType.OPERATION,
|
||||
handler(accessor) {
|
||||
const docFormulaPopupService = accessor.get(DocFormulaPopupService);
|
||||
return docFormulaPopupService.closePopup(true);
|
||||
},
|
||||
};
|
||||
|
||||
export const ConfirmFormulaPopupCommand: ICommand = {
|
||||
id: 'doc.operation.confirm-formula-popup',
|
||||
type: CommandType.COMMAND,
|
||||
handler(accessor) {
|
||||
const docFormulaPopupService = accessor.get(DocFormulaPopupService);
|
||||
return docFormulaPopupService.confirmPopup();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { CustomRangeType, Disposable, ICommandService, ILogService, Inject, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core';
|
||||
import type { IInsertCommandParams } from '@univerjs/docs';
|
||||
import { DeleteLeftCommand, InsertCommand, MoveCursorOperation, TextSelectionManagerService } from '@univerjs/docs';
|
||||
import { ComponentManager, IEditorService } from '@univerjs/ui';
|
||||
import { DocHoverManagerService } from '@univerjs/docs-ui';
|
||||
|
||||
import { AddDocUniFormulaCommand, RemoveDocUniFormulaCommand, UpdateDocUniFormulaCommand } from '../commands/command';
|
||||
import type { IShowFormulaPopupOperationParams } from '../commands/operation';
|
||||
import { CloseFormulaPopupOperation, ConfirmFormulaPopupCommand, ShowFormulaPopupOperation } from '../commands/operation';
|
||||
import { DocFormulaPopup, DOCS_UNI_FORMULA_EDITOR_UNIT_ID_KEY } from '../views/components/DocFormulaPopup';
|
||||
import { DocFormulaPopupService } from '../services/formula-popup.service';
|
||||
|
||||
const FORMULA_INPUT_TRIGGER_CHAR = '=';
|
||||
|
||||
@OnLifecycle(LifecycleStages.Steady, DocUniFormulaController)
|
||||
export class DocUniFormulaController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@Inject(DocHoverManagerService) private readonly _docHoverManagerService: DocHoverManagerService,
|
||||
@Inject(DocFormulaPopupService) private readonly _docFormulaPopupService: DocFormulaPopupService,
|
||||
@Inject(TextSelectionManagerService) private readonly _textSelectionManagerService: TextSelectionManagerService,
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initKeyboardListeners();
|
||||
this._initComponents();
|
||||
this._initCommands();
|
||||
this._initHoverListener();
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
ShowFormulaPopupOperation,
|
||||
CloseFormulaPopupOperation,
|
||||
ConfirmFormulaPopupCommand,
|
||||
AddDocUniFormulaCommand,
|
||||
RemoveDocUniFormulaCommand,
|
||||
UpdateDocUniFormulaCommand,
|
||||
].forEach((command) => this._commandService.registerCommand(command));
|
||||
}
|
||||
|
||||
private _initComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(DocFormulaPopup.componentKey, DocFormulaPopup));
|
||||
}
|
||||
|
||||
private _initKeyboardListeners(): void {
|
||||
// The formula input trigger works not exactly the same as Mention.
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
|
||||
const currentEditor = this._editorService.getFocusEditor();
|
||||
const focusedUnit = this._instanceSrv.getFocusedUnit();
|
||||
|
||||
const { id } = commandInfo;
|
||||
|
||||
if (
|
||||
currentEditor?.editorUnitId === DOCS_UNI_FORMULA_EDITOR_UNIT_ID_KEY ||
|
||||
focusedUnit?.type !== UniverInstanceType.UNIVER_DOC
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === InsertCommand.id) {
|
||||
const params = commandInfo.params as IInsertCommandParams;
|
||||
const activeRange = this._textSelectionManagerService.getActiveTextRange();
|
||||
if (params.body.dataStream === FORMULA_INPUT_TRIGGER_CHAR && activeRange) {
|
||||
this._showPopup({
|
||||
startIndex: activeRange.startOffset! - 1,
|
||||
unitId: focusedUnit.getUnitId(),
|
||||
});
|
||||
} else if (this._docFormulaPopupService.popupInfo) {
|
||||
this._closePopup();
|
||||
}
|
||||
}
|
||||
|
||||
if (id === MoveCursorOperation.id || id === DeleteLeftCommand.id) {
|
||||
this._closePopup();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _initHoverListener(): void {
|
||||
this.disposeWithMe(this._docHoverManagerService.activeCustomRanges$.subscribe((customRanges) => {
|
||||
const focusedUnit = this._instanceSrv.getFocusedUnit();
|
||||
|
||||
if (
|
||||
!focusedUnit ||
|
||||
this._docFormulaPopupService.popupInfo?.type === 'new' ||
|
||||
this._docFormulaPopupService.popupLocked
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formulaCustomRange = customRanges.find((range) => range.rangeType === CustomRangeType.UNI_FORMULA);
|
||||
if (formulaCustomRange) {
|
||||
const { startIndex, rangeId } = formulaCustomRange;
|
||||
this._logService.debug('[DocUniFormulaController]: activeCustomRanges', customRanges);
|
||||
this._showPopup({
|
||||
startIndex,
|
||||
unitId: focusedUnit.getUnitId(),
|
||||
rangeId,
|
||||
type: 'existing',
|
||||
});
|
||||
} else {
|
||||
if (!this._hovered) {
|
||||
this._closePopup(500);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._docFormulaPopupService.popupHovered$.subscribe((hovered) => {
|
||||
if (hovered) {
|
||||
this._removeTimer();
|
||||
}
|
||||
|
||||
this._hovered = hovered;
|
||||
}));
|
||||
}
|
||||
|
||||
private _hovered = false;
|
||||
|
||||
private _removeTimer(): void {
|
||||
if (this._closePopupTimer !== null) {
|
||||
window.clearTimeout(this._closePopupTimer);
|
||||
this._closePopupTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _showPopup(params: IShowFormulaPopupOperationParams): void {
|
||||
this._removeTimer();
|
||||
this._commandService.executeCommand(ShowFormulaPopupOperation.id, params);
|
||||
}
|
||||
|
||||
private _closePopupTimer: number | null = null;
|
||||
private _closePopup(timeout: number = 0): void {
|
||||
if (!this._docFormulaPopupService.popupInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeout === 0) {
|
||||
this._commandService.executeCommand(CloseFormulaPopupOperation.id);
|
||||
} else {
|
||||
this._closePopupTimer = window.setTimeout(() => this._closePopup(0), timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 = {
|
||||
'uni-formula': {
|
||||
popup: {
|
||||
title: {
|
||||
new: 'Create new formula',
|
||||
existing: 'Edit formula',
|
||||
},
|
||||
placeholder: 'Click here to insert formula or continue typing to close...',
|
||||
button: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
command: {
|
||||
'stream-placeholder': 'Doc Formula',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 = {
|
||||
'uni-formula': {
|
||||
popup: {
|
||||
title: {
|
||||
new: 'Create new formula',
|
||||
existing: 'Edit formula',
|
||||
},
|
||||
placeholder: 'Click here to insert formula or continue typing to close...',
|
||||
button: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
command: {
|
||||
'stream-placeholder': 'Doc Formula',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 = {
|
||||
'uni-formula': {
|
||||
popup: {
|
||||
title: {
|
||||
new: 'Create new formula',
|
||||
existing: 'Edit formula',
|
||||
},
|
||||
placeholder: 'Click here to insert formula or continue typing to close...',
|
||||
button: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
command: {
|
||||
'stream-placeholder': 'Doc Formula',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 = {
|
||||
'uni-formula': {
|
||||
popup: {
|
||||
title: {
|
||||
new: '添加公式',
|
||||
existing: '编辑公式',
|
||||
},
|
||||
placeholder: '点击此处添加公式,或继续输入以关闭',
|
||||
button: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
command: {
|
||||
'stream-placeholder': '文档公式',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 = {
|
||||
'uni-formula': {
|
||||
popup: {
|
||||
title: {
|
||||
new: 'Create new formula',
|
||||
existing: 'Edit formula',
|
||||
},
|
||||
placeholder: 'Click here to insert formula or continue typing to close...',
|
||||
button: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
command: {
|
||||
'stream-placeholder': 'Doc Formula',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { Disposable, FORMULA_EDITOR_ACTIVATED, ICommandService, IContextService, ILogService, Inject } from '@univerjs/core';
|
||||
import { makeSelection } from '@univerjs/docs';
|
||||
import { DocCanvasPopManagerService } from '@univerjs/docs-ui';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import type { IShortcutItem } from '@univerjs/ui';
|
||||
import { IShortcutService, KeyCode } from '@univerjs/ui';
|
||||
import { FORMULA_PROMPT_ACTIVATED } from '@univerjs/sheets-formula';
|
||||
import { UniFormulaService } from '@univerjs/uni-formula';
|
||||
import type { IAddDocUniFormulaCommandParams } from '../commands/command';
|
||||
import { AddDocUniFormulaCommand } from '../commands/command';
|
||||
import { ConfirmFormulaPopupCommand } from '../commands/operation';
|
||||
|
||||
export const DOC_FORMULA_POPUP_KEY = 'DOC_FORMULA_POPUP' as const;
|
||||
|
||||
export interface IDocFormulaPopupInfo {
|
||||
unitId: string;
|
||||
|
||||
/** If the popup is for inserting a formula or inspecting an existing formula. */
|
||||
type: 'new' | 'existing';
|
||||
|
||||
f: Nullable<string>;
|
||||
|
||||
disposable: IDisposable;
|
||||
|
||||
startIndex: number;
|
||||
}
|
||||
|
||||
export class DocFormulaPopupService extends Disposable {
|
||||
private readonly _popupInfo$ = new BehaviorSubject<Nullable<IDocFormulaPopupInfo>>(null);
|
||||
readonly popupInfo$ = this._popupInfo$.asObservable();
|
||||
get popupInfo(): Nullable<IDocFormulaPopupInfo> { return this._popupInfo$.getValue(); }
|
||||
|
||||
private _popupLocked = false;
|
||||
get popupLocked(): boolean { return this._popupLocked; }
|
||||
|
||||
private readonly _popupHovered$ = new Subject<boolean>();
|
||||
readonly popupHovered$ = this._popupHovered$.asObservable();
|
||||
|
||||
private _cachedFormulaString = '';
|
||||
|
||||
constructor(
|
||||
@Inject(DocCanvasPopManagerService) private readonly _docCanvasPopupManagerService: DocCanvasPopManagerService,
|
||||
@Inject(UniFormulaService) private readonly _uniFormulaService: UniFormulaService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService
|
||||
) {
|
||||
super();
|
||||
|
||||
const UniFormulaConfirmShortcut: IShortcutItem = {
|
||||
id: ConfirmFormulaPopupCommand.id,
|
||||
binding: KeyCode.ENTER,
|
||||
description: 'shortcut.doc.confirm-formula-popup',
|
||||
preconditions: (contextService) =>
|
||||
!contextService.getContextValue(FORMULA_PROMPT_ACTIVATED) && this.canConfirmPopup(),
|
||||
priority: 10000,
|
||||
};
|
||||
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut(UniFormulaConfirmShortcut));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._popupInfo$.next(null);
|
||||
this._popupInfo$.complete();
|
||||
|
||||
this._popupHovered$.complete();
|
||||
}
|
||||
|
||||
cacheFormulaString(f: string): void {
|
||||
this._cachedFormulaString = f;
|
||||
}
|
||||
|
||||
hoverPopup(hovered: boolean): void {
|
||||
this._popupHovered$.next(hovered);
|
||||
}
|
||||
|
||||
showPopup(unitId: string, startIndex: number, type: 'new'): boolean;
|
||||
showPopup(unitId: string, startIndex: number, type: 'existing', rangeId: string): boolean;
|
||||
showPopup(unitId: string, startIndex: number, type: 'new' | 'existing', rangeId?: string): boolean;
|
||||
showPopup(unitId: string, startIndex: number, type: 'new' | 'existing', rangeId?: string): boolean {
|
||||
this.closePopup();
|
||||
|
||||
const f = (rangeId && type === 'existing')
|
||||
? this._uniFormulaService.getFormulaWithRangeId(unitId, rangeId)?.f ?? '='
|
||||
: '=';
|
||||
const disposable = this._docCanvasPopupManagerService.attachPopupToRange(makeSelection(startIndex), {
|
||||
componentKey: DOC_FORMULA_POPUP_KEY,
|
||||
onClickOutside: () => this.closePopup(), // user may update ref range selections
|
||||
direction: 'top',
|
||||
});
|
||||
|
||||
this._popupInfo$.next({ unitId, disposable, type, f, startIndex });
|
||||
return true;
|
||||
}
|
||||
|
||||
lockPopup(): void {
|
||||
this._popupLocked = true;
|
||||
this._contextService.setContextValue(FORMULA_EDITOR_ACTIVATED, true);
|
||||
}
|
||||
|
||||
canConfirmPopup(): boolean {
|
||||
return this._cachedFormulaString !== '';
|
||||
}
|
||||
|
||||
async confirmPopup(): Promise<boolean> {
|
||||
const info = this.popupInfo;
|
||||
if (!info) return true;
|
||||
|
||||
const f = this._cachedFormulaString;
|
||||
if (!f) {
|
||||
this._logService.warn('[FormulaPopupService]: cannot write empty formula into the field.');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.unlockPopup();
|
||||
this.closePopup();
|
||||
|
||||
// write this formula string to doc
|
||||
return this._commandService.executeCommand(AddDocUniFormulaCommand.id, {
|
||||
unitId: info.unitId,
|
||||
f,
|
||||
startIndex: info.startIndex,
|
||||
} as IAddDocUniFormulaCommandParams);
|
||||
}
|
||||
|
||||
unlockPopup(): void {
|
||||
this._popupLocked = false;
|
||||
}
|
||||
|
||||
closePopup(force = false): boolean {
|
||||
if (this._popupLocked && !force) return false;
|
||||
|
||||
this._popupLocked = false;
|
||||
this._cachedFormulaString = '';
|
||||
|
||||
this.popupInfo?.disposable.dispose();
|
||||
this._popupInfo$.next(null);
|
||||
this._popupHovered$.next(false);
|
||||
|
||||
this._contextService.setContextValue(FORMULA_EDITOR_ACTIVATED, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import { DependentOn, Inject, Injector, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocUniFormulaPlugin } from '@univerjs/uni-formula';
|
||||
import { DOC_FORMULA_UI_PLUGIN_NAME } from './const';
|
||||
import { DocFormulaPopupService } from './services/formula-popup.service';
|
||||
import { DocUniFormulaController } from './controllers/formula-input.controller';
|
||||
|
||||
@DependentOn(UniverDocUniFormulaPlugin)
|
||||
export class UniverDocUniFormulaUIPlugin extends Plugin {
|
||||
static override pluginName: string = DOC_FORMULA_UI_PLUGIN_NAME;
|
||||
static override type: UniverInstanceType = UniverInstanceType.UNIVER_DOC;
|
||||
static override type: UniverInstanceType = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: unknown,
|
||||
@@ -29,4 +32,11 @@ export class UniverDocUniFormulaUIPlugin extends Plugin {
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(injector: Injector): void {
|
||||
([
|
||||
[DocUniFormulaController],
|
||||
[DocFormulaPopupService],
|
||||
] as Dependency[]).forEach((d) => injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { TextEditor, useObservable } from '@univerjs/ui';
|
||||
import type { IDocumentData, Nullable } from '@univerjs/core';
|
||||
import { BooleanNumber, createInternalEditorID, DEFAULT_EMPTY_DOCUMENT_VALUE, DocumentFlavor, HorizontalAlign, ICommandService, LocaleService, useDependency, VerticalAlign, WrapStrategy } from '@univerjs/core';
|
||||
import { CheckMarkSingle, CloseSingle } from '@univerjs/icons';
|
||||
import type { IDocFormulaPopupInfo } from '../../services/formula-popup.service';
|
||||
import { DOC_FORMULA_POPUP_KEY, DocFormulaPopupService } from '../../services/formula-popup.service';
|
||||
|
||||
import { CloseFormulaPopupOperation, ConfirmFormulaPopupCommand } from '../../commands/operation';
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const DOCS_UNI_FORMULA_EDITOR_UNIT_ID_KEY = createInternalEditorID('UNI_FORMULA');
|
||||
function makeSnapshot(f: string): IDocumentData {
|
||||
return {
|
||||
id: DOCS_UNI_FORMULA_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: `${f}${DEFAULT_EMPTY_DOCUMENT_VALUE}`,
|
||||
textRuns: [],
|
||||
paragraphs: [
|
||||
{
|
||||
startIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.MODERN,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
marginRight: 0,
|
||||
marginLeft: 0,
|
||||
paragraphLineGapDefault: 0,
|
||||
pageSize: {
|
||||
width: Number.POSITIVE_INFINITY,
|
||||
height: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
renderConfig: {
|
||||
horizontalAlign: HorizontalAlign.UNSPECIFIED,
|
||||
verticalAlign: VerticalAlign.TOP,
|
||||
centerAngle: 0,
|
||||
vertexAngle: 0,
|
||||
wrapStrategy: WrapStrategy.OVERFLOW,
|
||||
isRenderStyle: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function DocFormulaPopup() {
|
||||
const docFormulaPopupService = useDependency(DocFormulaPopupService);
|
||||
const popupInfo = useObservable(docFormulaPopupService.popupInfo$);
|
||||
|
||||
if (!popupInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DocFormula popupInfo={popupInfo} />;
|
||||
}
|
||||
|
||||
DocFormulaPopup.componentKey = DOC_FORMULA_POPUP_KEY;
|
||||
|
||||
function DocFormula(props: { popupInfo: IDocFormulaPopupInfo }) {
|
||||
const { popupInfo } = props;
|
||||
const { f } = popupInfo;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const formulaPopupService = useDependency(DocFormulaPopupService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const [formulaString, setFormulaString] = useState<Nullable<string>>(f);
|
||||
const snapshotRef = useRef(makeSnapshot(f ?? ''));
|
||||
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
const onFormulaStringChange = useCallback((formulaString: string) => {
|
||||
setFormulaString(formulaString);
|
||||
formulaPopupService.cacheFormulaString(formulaString);
|
||||
}, [formulaPopupService]);
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
// TODO: call operation instead
|
||||
commandService.executeCommand(ConfirmFormulaPopupCommand.id);
|
||||
}, [commandService]);
|
||||
|
||||
const onHovered = useCallback((hovered: boolean) => {
|
||||
formulaPopupService.hoverPopup(hovered);
|
||||
}, [formulaPopupService]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
commandService.executeCommand(CloseFormulaPopupOperation.id);
|
||||
}, [commandService]);
|
||||
|
||||
const handleEscKey = useCallback((event: { key: string }) => {
|
||||
if (event.key === 'Escape') {
|
||||
onCancel();
|
||||
}
|
||||
}, [onCancel]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
};
|
||||
}, [handleEscKey]);
|
||||
|
||||
return (
|
||||
<div className={styles.docUiFormulaPopup} onMouseEnter={() => onHovered(true)} onMouseLeave={() => onHovered(false)}>
|
||||
<TextEditor
|
||||
id={DOCS_UNI_FORMULA_EDITOR_UNIT_ID_KEY}
|
||||
className={clsx(styles.docUiFormulaPopupEditor, focused && styles.docUiFormulaPopupEditorActivated)}
|
||||
placeholder={localeService.t('uni-formula.popup.placeholder')}
|
||||
snapshot={snapshotRef.current}
|
||||
cancelDefaultResizeListener
|
||||
value={f ?? ''}
|
||||
isSingle
|
||||
isFormulaEditor
|
||||
onChange={(str) => onFormulaStringChange(str ?? '')}
|
||||
onFocus={() => {
|
||||
formulaPopupService.lockPopup();
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlur={() => setFocused(false)}
|
||||
/>
|
||||
<div className={clsx(styles.formulaIcon, { [styles.formulaIconDisable]: !formulaString })}>
|
||||
<span
|
||||
className={clsx(styles.iconContainer, styles.iconContainerError)}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<CloseSingle />
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={clsx(styles.iconContainer, styles.iconContainerSuccess)}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<CheckMarkSingle />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
.doc-ui-formula-popup {
|
||||
overflow: hidden;
|
||||
|
||||
width: 482px;
|
||||
display: flex;
|
||||
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--, #ecedef);
|
||||
background: #fff;
|
||||
height: 48px;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
gap: 8px;
|
||||
box-shadow: 0px 2px 4px 0px rgba(30, 34, 43, 0.06);
|
||||
|
||||
&-title {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
&-editor {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(---Hyacinth-Blue-500, #274fee);
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 153.846% */
|
||||
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
border-radius: var(--radius-m, 6px);
|
||||
border: 1px solid var(---Hyacinth-Blue-500, #274fee);
|
||||
background: #f0f3fe;
|
||||
box-shadow: 0px 0px 4px 0px rgba(39, 79, 238, 0.4);
|
||||
|
||||
// &-activated {
|
||||
// border-color: rgb(var(--primary-color));
|
||||
// }
|
||||
}
|
||||
|
||||
&-button-grp {
|
||||
margin-top: var(--margin-xs);
|
||||
text-align: right;
|
||||
text-wrap: nowrap;
|
||||
|
||||
button:not(:last-of-type) {
|
||||
margin-right: var(--margin-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.formula-icon {
|
||||
display: flex;
|
||||
}
|
||||
.icon-container {
|
||||
display: flex;
|
||||
padding: var(--radius-m, 6px);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background: var(--hyacinth-blue-5007, rgba(39, 79, 238, 0.07));
|
||||
color: #274fee;
|
||||
}
|
||||
}
|
||||
}
|
||||
-4
@@ -14,7 +14,3 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class DocFormulaInputController {
|
||||
|
||||
}
|
||||
|
||||
@@ -4,4 +4,9 @@ import pkg from './package.json';
|
||||
export default ({ mode }) => createViteConfig({}, {
|
||||
mode,
|
||||
pkg,
|
||||
features: {
|
||||
react: false,
|
||||
css: true,
|
||||
dom: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@univerjs/uni-formula",
|
||||
"version": "0.0.1",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"author": "DreamNum <developer@univer.ai>",
|
||||
@@ -61,6 +61,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/engine-formula": "workspace:*",
|
||||
"@univerjs/rpc": "workspace:*",
|
||||
"@univerjs/sheets-formula": "workspace:*"
|
||||
|
||||
@@ -30,9 +30,9 @@ export interface IAddDocUniFormulaMutationParams {
|
||||
export const AddDocUniFormulaMutation: IMutation<IAddDocUniFormulaMutationParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'doc.mutation.add-doc-uni-formula',
|
||||
handler(accesstor, params: IAddDocUniFormulaMutationParams) {
|
||||
handler(accessor, params: IAddDocUniFormulaMutationParams) {
|
||||
const { unitId, f, rangeId: id } = params;
|
||||
const uniFormulaService = accesstor.get(UniFormulaService);
|
||||
const uniFormulaService = accessor.get(UniFormulaService);
|
||||
|
||||
uniFormulaService.registerDocFormula(unitId, id, f);
|
||||
|
||||
|
||||
@@ -15,4 +15,18 @@
|
||||
*/
|
||||
|
||||
export { UniverDocUniFormulaPlugin } from './uni-formula.plugin';
|
||||
export { UniFormulaService, type IUpdateDocUniFormulaCacheMutationParams } from './services/uni-formula.service';
|
||||
export { DOC_FORMULA_PLUGIN_NAME } from './const';
|
||||
|
||||
// #region - all commands
|
||||
|
||||
export {
|
||||
type IAddDocUniFormulaMutationParams,
|
||||
type IRemoveDocUniFormulaMutationParams,
|
||||
type IUpdateDocUniFormulaMutationParams,
|
||||
AddDocUniFormulaMutation,
|
||||
RemoveDocUniFormulaMutation,
|
||||
UpdateDocUniFormulaMutation,
|
||||
} from './commands/mutation';
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -14,13 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICellData, IDisposable, IMutation, Nullable } from '@univerjs/core';
|
||||
import { CommandType, Disposable, ICommandService, Inject, IUniverInstanceService, LifecycleService, LifecycleStages, OnLifecycle, Optional, RCDisposable, ResourceManagerService, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { DataSyncPrimaryController } from '@univerjs/rpc';
|
||||
import { RegisterOtherFormulaService } from '@univerjs/sheets-formula';
|
||||
import type { DocumentDataModel, ICellData, IDisposable, IDocumentBody, IMutation, Nullable } from '@univerjs/core';
|
||||
import {
|
||||
CommandType,
|
||||
CustomRangeType,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IResourceManagerService,
|
||||
IUniverInstanceService,
|
||||
LifecycleStages,
|
||||
makeCustomRangeStream,
|
||||
OnLifecycle,
|
||||
Optional,
|
||||
RCDisposable,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { makeSelection, replaceSelectionFactory } from '@univerjs/docs';
|
||||
|
||||
import { type IDocFormulaCache, type IDocFormulaData, type IDocFormulaReference, toJson } from '../models/doc-formula';
|
||||
import { DOC_FORMULA_PLUGIN_NAME } from '../const';
|
||||
import { AddDocUniFormulaMutation, RemoveDocUniFormulaMutation, UpdateDocUniFormulaMutation } from '../commands/mutation';
|
||||
|
||||
const DOC_PSEUDO_SUBUNIT = 'DOC_PSEUDO_SUBUNIT';
|
||||
|
||||
@@ -28,18 +45,66 @@ const DOC_PSEUDO_SUBUNIT = 'DOC_PSEUDO_SUBUNIT';
|
||||
* Update calculating result in a batch.
|
||||
*/
|
||||
export interface IUpdateDocUniFormulaCacheMutationParams {
|
||||
/** The doc in which formula results changed. */
|
||||
unitId: string;
|
||||
/** Range ids. */
|
||||
ids: string[];
|
||||
/** Calculation results. */
|
||||
cache: IDocFormulaCache[];
|
||||
}
|
||||
|
||||
export const UpdateDocUniFormulaCacheMutation: IMutation<IUpdateDocUniFormulaCacheMutationParams> = {
|
||||
/** This mutation is internal. It should not be exposed to third-party developers. */
|
||||
const UpdateDocUniFormulaCacheMutation: IMutation<IUpdateDocUniFormulaCacheMutationParams> = {
|
||||
type: CommandType.MUTATION,
|
||||
id: 'doc.mutation.update-doc-uni-formula-cache',
|
||||
handler(accessor, params: IUpdateDocUniFormulaCacheMutationParams) {
|
||||
const { unitId, ids, cache } = params;
|
||||
|
||||
const uniFormulaService = accessor.get(UniFormulaService);
|
||||
return uniFormulaService.updateFormulaResults(unitId, ids, cache);
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
const data = instanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
/** The document may have not loaded on this client. We are safe to ignore cache updating. */
|
||||
if (!data) return true;
|
||||
const body = data.getBody();
|
||||
|
||||
function getRange(rangeId: string) {
|
||||
return body?.customRanges?.find((r) => r.rangeId === rangeId);
|
||||
}
|
||||
|
||||
const saveCacheResult = uniFormulaService.updateFormulaResults(unitId, ids, cache);
|
||||
if (!saveCacheResult) return false;
|
||||
|
||||
return ids.every((id, index) => {
|
||||
const range = getRange(id);
|
||||
if (!range) return true; // If we cannot find that rangeId, we are save to ignore cache.
|
||||
|
||||
const dataStream = makeCustomRangeStream(`${cache[index].v ?? ''}`);
|
||||
const body: IDocumentBody = {
|
||||
dataStream,
|
||||
customRanges: [{
|
||||
startIndex: 0,
|
||||
endIndex: dataStream.length - 1,
|
||||
rangeId: id,
|
||||
rangeType: CustomRangeType.UNI_FORMULA,
|
||||
wholeEntity: true,
|
||||
}],
|
||||
};
|
||||
|
||||
const redoMutation = replaceSelectionFactory(accessor, {
|
||||
unitId,
|
||||
body,
|
||||
selection: makeSelection(range.startIndex, range.endIndex),
|
||||
});
|
||||
|
||||
if (redoMutation) {
|
||||
return commandService.syncExecuteCommand(redoMutation.id, redoMutation.params, { onlyLocal: true });
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,15 +112,14 @@ export const UpdateDocUniFormulaCacheMutation: IMutation<IUpdateDocUniFormulaCac
|
||||
* This service provides methods for docs and slides to register a formula into
|
||||
* Univer's formula system.
|
||||
*/
|
||||
@OnLifecycle(LifecycleStages.Starting, UniFormulaService)
|
||||
@OnLifecycle(LifecycleStages.Steady, UniFormulaService)
|
||||
export class UniFormulaService extends Disposable {
|
||||
/** This data maps doc formula key to the formula id in the formula system. */
|
||||
private readonly _docFormulas = new Map<string, IDocFormulaReference>();
|
||||
private readonly _formulaIdToKey = new Map<string, string>();
|
||||
|
||||
constructor(
|
||||
@Inject(ResourceManagerService) resoureManagerService: ResourceManagerService,
|
||||
@Inject(LifecycleService) private readonly _lifecycleService: LifecycleService,
|
||||
@IResourceManagerService resourceManagerService: IResourceManagerService,
|
||||
// TODO: wzhudev We may need to register the doc formulas to formula service in later lifecycle stages.
|
||||
@ICommandService private readonly _commandSrv: ICommandService,
|
||||
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
|
||||
@@ -64,11 +128,9 @@ export class UniFormulaService extends Disposable {
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(this._commandSrv.registerCommand(UpdateDocUniFormulaCacheMutation));
|
||||
|
||||
this._initDocFormulaResources(resoureManagerService);
|
||||
this._initCommands();
|
||||
this._initDocFormulaResources(resourceManagerService);
|
||||
this._initDocDisposingListener();
|
||||
this._debugInitFormula();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
@@ -77,16 +139,29 @@ export class UniFormulaService extends Disposable {
|
||||
// TODO: wzhudev: concrete disposing methods
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
AddDocUniFormulaMutation,
|
||||
UpdateDocUniFormulaMutation,
|
||||
UpdateDocUniFormulaCacheMutation,
|
||||
RemoveDocUniFormulaMutation,
|
||||
].forEach((command) => this._commandSrv.registerCommand(command));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all formulas under a doc.
|
||||
*/
|
||||
private _unregisterDoc(unitId: string): void {
|
||||
const existingsFormulas = Array.from(this._docFormulas.entries());
|
||||
existingsFormulas.forEach(([_, value]) => {
|
||||
const existingFormulas = Array.from(this._docFormulas.entries());
|
||||
existingFormulas.forEach(([_, value]) => {
|
||||
if (value.unitId === unitId) this.unregisterDocFormula(unitId, value.rangeId);
|
||||
});
|
||||
}
|
||||
|
||||
getFormulaWithRangeId(unitId: string, rangeId: string): Nullable<IDocFormulaReference> {
|
||||
return this._docFormulas.get(getDocFormulaKey(unitId, rangeId)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a doc formula into the formula system.
|
||||
*/
|
||||
@@ -128,15 +203,16 @@ export class UniFormulaService extends Disposable {
|
||||
}
|
||||
|
||||
updateFormulaResults(unitId: string, formulaIds: string[], v: IDocFormulaCache[]): boolean {
|
||||
// TODO: @wzhudev: should trigger re-render
|
||||
return formulaIds.every((id, index) => {
|
||||
formulaIds.forEach((id, index) => {
|
||||
const formulaData = this._docFormulas.get(getDocFormulaKey(unitId, id));
|
||||
if (!formulaData) return false;
|
||||
if (!formulaData) return true;
|
||||
|
||||
formulaData.v = v[index].v;
|
||||
formulaData.t = v[index].t;
|
||||
return true;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dataSyncDisposables = new Map<string, RCDisposable>();
|
||||
@@ -200,11 +276,7 @@ export class UniFormulaService extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _debugInitFormula(): void {
|
||||
this.registerDocFormula('test_doc', 'test_formula_id', '=\'[workbook-01]工作表11\'!A13');
|
||||
}
|
||||
|
||||
private _initDocFormulaResources(resourceManagerService: ResourceManagerService): void {
|
||||
private _initDocFormulaResources(resourceManagerService: IResourceManagerService): void {
|
||||
resourceManagerService.registerPluginResource({
|
||||
pluginName: DOC_FORMULA_PLUGIN_NAME,
|
||||
businesses: [UniverInstanceType.UNIVER_DOC],
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
|
||||
import { DependentOn, Inject, Injector, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
||||
import { UniFormulaService } from './services/uni-formula.service';
|
||||
import { DOC_FORMULA_PLUGIN_NAME } from './const';
|
||||
|
||||
@DependentOn(UniverFormulaEnginePlugin)
|
||||
@DependentOn(UniverFormulaEnginePlugin, UniverSheetsFormulaPlugin)
|
||||
export class UniverDocUniFormulaPlugin extends Plugin {
|
||||
static override pluginName: string = DOC_FORMULA_PLUGIN_NAME;
|
||||
static override type: UniverInstanceType = UniverInstanceType.UNIVER_DOC;
|
||||
|
||||
// This plugin should load only when sheet related modules are loaded.
|
||||
static override type: UniverInstanceType = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: unknown,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# @univerjs/uni-sheets-ui
|
||||
|
||||
[](https://npmjs.org/packages/@univerjs/uni-sheets-ui)
|
||||
[](https://img.shields.io/npm/l/@univerjs/uni-sheets-ui)
|
||||
|
||||
## Introduction
|
||||
|
||||
> TODO: Introduction
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```shell
|
||||
npm i @univerjs/uni-sheets-ui
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "@univerjs/uni-sheets-ui",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"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/*"
|
||||
},
|
||||
"main": "./lib/cjs/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/cjs/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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-drawing-ui": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/icons": "^0.1.72"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/shared": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"vitest": "^2.0.3"
|
||||
},
|
||||
"univerSpace": {
|
||||
".": {
|
||||
"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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { BooleanNumber, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, EDITOR_ACTIVATED, FOCUSING_SHEET, FontItalic, FontWeight, ICommandService, IContextService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { SetInlineFormatCommand, SetTextSelectionsOperation, TextSelectionManagerService } from '@univerjs/docs';
|
||||
import { RangeProtectionPermissionEditPoint, SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetActiveOperation, SheetsSelectionsService, WorkbookEditablePermission, WorksheetEditPermission, WorksheetSetCellStylePermission } from '@univerjs/sheets';
|
||||
import { deriveStateFromActiveSheet$, getCurrentRangeDisable$ } from '@univerjs/sheets-ui';
|
||||
import { getMenuHiddenObservable, type IMenuButtonItem, MenuGroup, MenuItemType } from '@univerjs/ui';
|
||||
import { FONT_GROUP_MENU_ID } from '@univerjs/uniui';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export const SHEET_BOLD_MUTATION_ID = 'sheet.command.uni-bold';
|
||||
export const SHEET_ITALIC_MUTATION_ID = 'sheet.command.uni-italic';
|
||||
export const SHEET_UNDERLINE_MUTATION_ID = 'sheet.command.uni-underline';
|
||||
export const SHEET_STRIKE_MUTATION_ID = 'sheet.command.uni-strike';
|
||||
|
||||
export function SheetBoldMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
return {
|
||||
id: SHEET_BOLD_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'BoldSingle',
|
||||
title: 'Set bold',
|
||||
tooltip: 'toolbar.bold',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission], rangeTypes: [RangeProtectionPermissionEditPoint] }),
|
||||
activated$: deriveStateFromActiveSheet$(univerInstanceService, false, ({ worksheet }) => new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
if (id === SetRangeValuesMutation.id || id === SetSelectionsOperation.id || id === SetWorksheetActiveOperation.id) {
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let isBold = FontWeight.NORMAL;
|
||||
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
isBold = range?.getFontWeight();
|
||||
}
|
||||
|
||||
subscriber.next(isBold === FontWeight.BOLD);
|
||||
}
|
||||
|
||||
if (
|
||||
(id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
contextService.getContextValue(FOCUSING_SHEET)
|
||||
) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
if (textRun == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bl = textRun.ts?.bl;
|
||||
subscriber.next(bl === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
if (!worksheet) {
|
||||
subscriber.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let isBold = FontWeight.NORMAL;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
isBold = range?.getFontWeight();
|
||||
}
|
||||
subscriber.next(isBold === FontWeight.BOLD);
|
||||
|
||||
return disposable.dispose;
|
||||
})),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
export function SheetItalicMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
|
||||
return {
|
||||
id: SHEET_ITALIC_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'ItalicSingle',
|
||||
title: 'Set italic',
|
||||
tooltip: 'toolbar.italic',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission], rangeTypes: [RangeProtectionPermissionEditPoint] }),
|
||||
activated$: deriveStateFromActiveSheet$(univerInstanceService, false, ({ worksheet }) => new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
if (id === SetRangeValuesMutation.id || id === SetSelectionsOperation.id || id === SetWorksheetActiveOperation.id) {
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let isItalic = FontItalic.NORMAL;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
isItalic = range?.getFontStyle();
|
||||
}
|
||||
|
||||
subscriber.next(isItalic === FontItalic.ITALIC);
|
||||
}
|
||||
|
||||
if (
|
||||
(id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
contextService.getContextValue(FOCUSING_SHEET)
|
||||
) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
if (textRun == null) return;
|
||||
|
||||
const it = textRun.ts?.it;
|
||||
subscriber.next(it === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let isItalic = FontItalic.NORMAL;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
isItalic = range?.getFontStyle();
|
||||
}
|
||||
|
||||
subscriber.next(isItalic === FontItalic.ITALIC);
|
||||
return disposable.dispose;
|
||||
})),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
export function SheetUnderlineMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
|
||||
return {
|
||||
id: SHEET_UNDERLINE_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'UnderlineSingle',
|
||||
title: 'Set underline',
|
||||
tooltip: 'toolbar.underline',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
activated$: deriveStateFromActiveSheet$(univerInstanceService, false, ({ worksheet }) => new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
if (id === SetRangeValuesMutation.id || id === SetSelectionsOperation.id || id === SetWorksheetActiveOperation.id) {
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let isUnderline;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
isUnderline = range?.getUnderline();
|
||||
}
|
||||
|
||||
subscriber.next(!!(isUnderline && isUnderline.s));
|
||||
}
|
||||
|
||||
if (
|
||||
(id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
contextService.getContextValue(FOCUSING_SHEET)
|
||||
) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
if (textRun == null) return;
|
||||
|
||||
const ul = textRun.ts?.ul;
|
||||
subscriber.next(ul?.s === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let isUnderline;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
isUnderline = range?.getUnderline();
|
||||
}
|
||||
|
||||
subscriber.next(!!(isUnderline && isUnderline.s));
|
||||
return disposable.dispose;
|
||||
})),
|
||||
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission], rangeTypes: [RangeProtectionPermissionEditPoint] }),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
export function SheetStrikeThroughMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
|
||||
return {
|
||||
id: SHEET_STRIKE_MUTATION_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'StrikethroughSingle',
|
||||
title: 'Set strike through',
|
||||
tooltip: 'toolbar.strikethrough',
|
||||
positions: [FONT_GROUP_MENU_ID],
|
||||
disabled$: getCurrentRangeDisable$(accessor, { workbookTypes: [WorkbookEditablePermission], worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission], rangeTypes: [RangeProtectionPermissionEditPoint] }),
|
||||
activated$: deriveStateFromActiveSheet$(univerInstanceService, false, ({ worksheet }) => new Observable<boolean>((subscriber) => {
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
if (id === SetRangeValuesMutation.id || id === SetSelectionsOperation.id || id === SetWorksheetActiveOperation.id) {
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let st;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
st = range?.getStrikeThrough();
|
||||
}
|
||||
|
||||
subscriber.next(!!(st && st.s));
|
||||
}
|
||||
|
||||
if (
|
||||
(id === SetTextSelectionsOperation.id || id === SetInlineFormatCommand.id) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
contextService.getContextValue(FOCUSING_SHEET)
|
||||
) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
|
||||
if (textRun == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const st = textRun.ts?.st;
|
||||
|
||||
subscriber.next(st?.s === BooleanNumber.TRUE);
|
||||
}
|
||||
});
|
||||
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
let st;
|
||||
if (primary != null) {
|
||||
const range = worksheet.getRange(primary.startRow, primary.startColumn);
|
||||
st = range?.getStrikeThrough();
|
||||
}
|
||||
|
||||
subscriber.next(!!(st && st.s));
|
||||
return disposable.dispose;
|
||||
})),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
function getFontStyleAtCursor(accessor: IAccessor) {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const textSelectionService = accessor.get(TextSelectionManagerService);
|
||||
|
||||
const editorDataModel = univerInstanceService.getUniverDocInstance(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
const activeTextRange = textSelectionService.getActiveRectRange();
|
||||
|
||||
if (editorDataModel == null || activeTextRange == null) return null;
|
||||
|
||||
const textRuns = editorDataModel.getBody()?.textRuns;
|
||||
if (textRuns == null) return;
|
||||
|
||||
const { startOffset } = activeTextRange;
|
||||
const textRun = textRuns.find(({ st, ed }) => startOffset! >= st && startOffset! <= ed);
|
||||
return textRun;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { connectInjector, ICommandService, Inject, Injector, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType, useDependency } from '@univerjs/core';
|
||||
import { type IUniverSheetsUIConfig, RenderSheetContent, SetRangeBoldCommand, SetRangeFontFamilyCommand, SetRangeFontSizeCommand, SetRangeItalicCommand, SetRangeStrickThroughCommand, SetRangeTextColorCommand, SetRangeUnderlineCommand, SheetUIController } from '@univerjs/sheets-ui';
|
||||
import type { IMenuItemFactory } from '@univerjs/ui';
|
||||
import { BuiltInUIPart, ComponentManager, ILayoutService, IMenuService, IShortcutService, IUIPartsService, useObservable } from '@univerjs/ui';
|
||||
import { BuiltinUniToolbarItemId, generateCloneMutation, UniToolbarService, UniUIPart } from '@univerjs/uniui';
|
||||
import React from 'react';
|
||||
import { SetBackgroundColorCommand } from '@univerjs/sheets';
|
||||
import { IMAGE_MENU_ID as SheetsImageMenuId } from '@univerjs/sheets-drawing-ui';
|
||||
import { UniSheetBar } from '../views/uni-sheet-bar/UniSheetBar';
|
||||
import { SHEET_BOLD_MUTATION_ID, SHEET_ITALIC_MUTATION_ID, SHEET_STRIKE_MUTATION_ID, SHEET_UNDERLINE_MUTATION_ID, SheetBoldMenuItemFactory, SheetItalicMenuItemFactory, SheetStrikeThroughMenuItemFactory, SheetUnderlineMenuItemFactory } from './menu';
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, SheetUIController)
|
||||
export class UniSheetsUIController extends SheetUIController {
|
||||
constructor(
|
||||
config: Partial<IUniverSheetsUIConfig>,
|
||||
@Inject(Injector) injector: Injector,
|
||||
@Inject(ComponentManager) componentManager: ComponentManager,
|
||||
@ILayoutService layoutService: ILayoutService,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IShortcutService shortcutService: IShortcutService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IUIPartsService uiPartsService: IUIPartsService,
|
||||
@Inject(UniToolbarService) private readonly _toolbarService: UniToolbarService
|
||||
) {
|
||||
super(
|
||||
config,
|
||||
injector,
|
||||
componentManager,
|
||||
layoutService,
|
||||
commandService,
|
||||
shortcutService,
|
||||
menuService,
|
||||
uiPartsService
|
||||
);
|
||||
this._initUniMenus();
|
||||
this._initMutations();
|
||||
}
|
||||
|
||||
protected override _initWorkbenchParts(): void {
|
||||
const uiController = this._uiPartsService;
|
||||
const injector = this._injector;
|
||||
|
||||
// this.disposeWithMe(uiController.registerComponent(BuiltInUIPart.HEADER, () => connectInjector(RenderSheetHeader, injector)));
|
||||
this.disposeWithMe(uiController.registerComponent(UniUIPart.OUTLINE, () => connectInjector(RenderOutline, injector)));
|
||||
this.disposeWithMe(uiController.registerComponent(BuiltInUIPart.CONTENT, () => connectInjector(RenderSheetContent, injector)));
|
||||
}
|
||||
|
||||
private _initUniMenus(): void {
|
||||
([
|
||||
[BuiltinUniToolbarItemId.FONT_FAMILY, SetRangeFontFamilyCommand.id],
|
||||
[BuiltinUniToolbarItemId.FONT_SIZE, SetRangeFontSizeCommand.id],
|
||||
[BuiltinUniToolbarItemId.COLOR, SetRangeTextColorCommand.id],
|
||||
[BuiltinUniToolbarItemId.BACKGROUND, SetBackgroundColorCommand.id],
|
||||
[BuiltinUniToolbarItemId.IMAGE, SheetsImageMenuId],
|
||||
]).forEach(([id, menuId]) => {
|
||||
this._toolbarService.implementItem(id, { id: menuId, type: UniverInstanceType.UNIVER_SHEET });
|
||||
});
|
||||
|
||||
(
|
||||
[
|
||||
SheetBoldMenuItemFactory,
|
||||
SheetItalicMenuItemFactory,
|
||||
SheetUnderlineMenuItemFactory,
|
||||
SheetStrikeThroughMenuItemFactory,
|
||||
] as IMenuItemFactory[]
|
||||
).forEach((factory) => {
|
||||
this.disposeWithMe(this._menuService.addMenuItem(this._injector.invoke(factory), {}));
|
||||
});
|
||||
}
|
||||
|
||||
private _initMutations() {
|
||||
[
|
||||
generateCloneMutation(SHEET_BOLD_MUTATION_ID, SetRangeBoldCommand),
|
||||
generateCloneMutation(SHEET_ITALIC_MUTATION_ID, SetRangeItalicCommand),
|
||||
generateCloneMutation(SHEET_UNDERLINE_MUTATION_ID, SetRangeUnderlineCommand),
|
||||
generateCloneMutation(SHEET_STRIKE_MUTATION_ID, SetRangeStrickThroughCommand),
|
||||
].forEach((mutation) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(mutation));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function RenderOutline() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const focused = useObservable(univerInstanceService.focused$);
|
||||
const workbook = useObservable(() => univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET), null, false, []);
|
||||
if (!workbook || focused !== workbook?.getUnitId()) return null;
|
||||
|
||||
return (
|
||||
<UniSheetBar />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { UniSheetsUIController } from './controllers/uni-sheets-ui.controller';
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { IBaseSheetBarProps } from '@univerjs/sheets-ui';
|
||||
import { useActiveWorkbook } from '@univerjs/sheets-ui';
|
||||
import { IncreaseSingle } from '@univerjs/icons';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ICommandInfo } from '@univerjs/core';
|
||||
import { ICommandService, useDependency } from '@univerjs/core';
|
||||
import { InsertSheetCommand, InsertSheetMutation, RemoveSheetMutation, SetTabColorMutation, SetWorksheetActiveOperation, SetWorksheetHideMutation, SetWorksheetNameMutation, SetWorksheetOrderMutation } from '@univerjs/sheets';
|
||||
import { Tooltip } from '@univerjs/design';
|
||||
import styles from './index.module.less';
|
||||
|
||||
export function UniSheetBar() {
|
||||
const [sheetList, setSheetList] = useState<IBaseSheetBarProps[]>([]);
|
||||
const [activeKey, setActiveKey] = useState('');
|
||||
const [barHeight, setBarHeight] = useState(0);
|
||||
const workbook = useActiveWorkbook()!;
|
||||
const commandService = useDependency(ICommandService);
|
||||
const sheetBarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const sheetBar = sheetBarRef.current;
|
||||
if (sheetBar) {
|
||||
setBarHeight(sheetBar.clientHeight - 38);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateSheetItems = useCallback(() => {
|
||||
const currentSubUnitId = workbook.getActiveSheet()?.getSheetId() || '';
|
||||
setActiveKey(currentSubUnitId);
|
||||
|
||||
const sheets = workbook.getSheets();
|
||||
const activeSheet = workbook.getActiveSheet();
|
||||
const sheetListItems = sheets
|
||||
.filter((sheet) => !sheet.isSheetHidden())
|
||||
.map((sheet, index) => {
|
||||
return {
|
||||
sheetId: sheet.getSheetId(),
|
||||
label: sheet.getName(),
|
||||
index,
|
||||
selected: activeSheet === sheet,
|
||||
color: sheet.getTabColor() ?? undefined,
|
||||
};
|
||||
});
|
||||
|
||||
setSheetList(sheetListItems);
|
||||
setActiveKey(currentSubUnitId);
|
||||
}, [workbook]);
|
||||
|
||||
useEffect(() => {
|
||||
updateSheetItems();
|
||||
const disposable = setupStatusUpdate();
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [workbook]);
|
||||
|
||||
const handleSheetActiveChange = (subUnitId: string | undefined) => {
|
||||
if (!subUnitId) return;
|
||||
|
||||
commandService.executeCommand(SetWorksheetActiveOperation.id, {
|
||||
subUnitId,
|
||||
unitId: workbook.getUnitId(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSheetAdd = () => {
|
||||
commandService.executeCommand(InsertSheetCommand.id);
|
||||
};
|
||||
|
||||
const setupStatusUpdate = () =>
|
||||
commandService.onCommandExecuted((commandInfo: ICommandInfo) => {
|
||||
switch (commandInfo.id) {
|
||||
case SetTabColorMutation.id:
|
||||
case SetWorksheetHideMutation.id:
|
||||
case RemoveSheetMutation.id:
|
||||
case SetWorksheetNameMutation.id:
|
||||
case InsertSheetMutation.id:
|
||||
case SetWorksheetOrderMutation.id:
|
||||
case SetWorksheetActiveOperation.id:
|
||||
updateSheetItems();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.uniSheetBar} ref={sheetBarRef}>
|
||||
<div className={styles.sheetTab} style={{ height: `${barHeight}px` }}>
|
||||
<div className={styles.sheetItems}>
|
||||
{sheetList.map((item, index) => (
|
||||
<div key={index} className={`${styles.sheetItem} ${activeKey === item.sheetId ? styles.active : ''}`} onClick={() => handleSheetActiveChange(item.sheetId)}>
|
||||
<Tooltip showIfEllipsis title={item.label} placement="right">
|
||||
<span>
|
||||
{' '}
|
||||
{item.label}
|
||||
{' '}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className={styles.newSheetButton} onClick={handleSheetAdd}>
|
||||
<IncreaseSingle className={styles.newSheetButtonIcon} />
|
||||
<span>New sheet</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
.uni-sheet-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
|
||||
.sheet-tab {
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
.sheet-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--margin-xxs);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
|
||||
.sheet-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 var(--margin-xs);
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 142px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-100));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: rgba(var(--hyacinth-500));
|
||||
background-color: rgb(var(--hyacinth-500), 0.07);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-sheet-button {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
margin-top: 16px;
|
||||
padding: 0 var(--margin-xs);
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: rgba(var(--hyacinth-500));
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-100));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -10px;
|
||||
height: 1px;
|
||||
background-color: #ecedef;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@univerjs/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@univerjs/shared/tsconfigs/node",
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import createViteConfig from '@univerjs/shared/vite';
|
||||
import pkg from './package.json';
|
||||
|
||||
export default ({ mode }) => createViteConfig({}, {
|
||||
mode,
|
||||
pkg,
|
||||
features: {
|
||||
react: false,
|
||||
css: true,
|
||||
dom: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
# @univerjs/uni-slides-ui
|
||||
|
||||
[](https://npmjs.org/packages/@univerjs/uni-slides-ui)
|
||||
[](https://img.shields.io/npm/l/@univerjs/uni-slides-ui)
|
||||
|
||||
## Introduction
|
||||
|
||||
> TODO: Introduction
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```shell
|
||||
npm i @univerjs/uni-slides-ui
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "@univerjs/uni-slides-ui",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"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": [],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"main": "./lib/cjs/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/cjs/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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"build": "tsc && vite build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/slides-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"clsx": ">=2.0.0",
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/icons": "^0.1.72"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/shared": "workspace:*",
|
||||
"@univerjs/slides": "workspace:*",
|
||||
"@univerjs/slides-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/uniui": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"less": "^4.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"univerSpace": {
|
||||
".": {
|
||||
"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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import type { SlideDataModel } from '@univerjs/core';
|
||||
import { connectInjector, ICommandService, Inject, Injector, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType, useDependency } from '@univerjs/core';
|
||||
import type { IUniverSlidesDrawingConfig } from '@univerjs/slides-ui';
|
||||
import { SlidesUIController } from '@univerjs/slides-ui';
|
||||
import { ComponentManager, IMenuService, IShortcutService, IUIPartsService, useObservable } from '@univerjs/ui';
|
||||
import { UniUIPart } from '@univerjs/uniui';
|
||||
import { UniSlideSideBar } from '../views/UniSlideSideBar';
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, UniSlidesUIController)
|
||||
export class UniSlidesUIController extends SlidesUIController {
|
||||
constructor(
|
||||
_config: Partial<IUniverSlidesDrawingConfig>,
|
||||
@Inject(Injector) _injector: Injector,
|
||||
@IMenuService _menuService: IMenuService,
|
||||
@Inject(ComponentManager) _componentManager: ComponentManager,
|
||||
@IUIPartsService _uiPartsService: IUIPartsService,
|
||||
@ICommandService _commandService: ICommandService,
|
||||
@IShortcutService _shortcutService: IShortcutService
|
||||
) {
|
||||
super(_config, _injector, _menuService, _componentManager, _uiPartsService, _commandService, _shortcutService);
|
||||
}
|
||||
|
||||
protected override _initUIComponents(): void {
|
||||
this.disposeWithMe(this._uiPartsService.registerComponent(UniUIPart.OUTLINE, () => connectInjector(RenderOutline, this._injector)));
|
||||
}
|
||||
}
|
||||
|
||||
function RenderOutline() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const focused = useObservable(univerInstanceService.focused$);
|
||||
const slide = useObservable(() => univerInstanceService.getCurrentTypeOfUnit$<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE), null, false, []);
|
||||
if (!slide || focused !== slide.getUnitId()) return null;
|
||||
|
||||
return (
|
||||
<UniSlideSideBar />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { UniSlidesUIController } from './controllers/uni-slides-ui.controller';
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { SlideDataModel } from '@univerjs/core';
|
||||
import clsx from 'clsx';
|
||||
import { ICommandService, IUniverInstanceService, UniverInstanceType, useDependency } from '@univerjs/core';
|
||||
import { Scrollbar } from '@univerjs/design';
|
||||
import type { RefObject } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useObservable } from '@univerjs/ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ActivateSlidePageOperation, SetSlidePageThumbOperation } from '@univerjs/slides-ui';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
/**
|
||||
* This components works as the root component of the left Sidebar of Slide.
|
||||
*/
|
||||
|
||||
export function UniSlideSideBar() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
|
||||
const slideBarRef = useRef<HTMLDivElement>(null);
|
||||
const currentSlide = useObservable(
|
||||
() => univerInstanceService.getCurrentTypeOfUnit$<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE),
|
||||
undefined,
|
||||
undefined,
|
||||
[]
|
||||
);
|
||||
const pages = currentSlide?.getPages();
|
||||
const pageOrder = currentSlide?.getPageOrder();
|
||||
|
||||
if (!pages || !pageOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slideList = pageOrder.map((id) => pages[id]);
|
||||
|
||||
const [divRefs, setDivRefs] = useState<RefObject<HTMLDivElement>[]>([]);
|
||||
const [activatePageId, setActivatePageId] = useState<string | null>(pageOrder[0]);
|
||||
|
||||
useEffect(() => {
|
||||
setDivRefs(slideList.map((_) => React.createRef()));
|
||||
}, [slideList.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscriber = currentSlide?.activePage$.subscribe((page) => {
|
||||
const id = page?.id ?? null;
|
||||
|
||||
id && setActivatePageId(id);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriber?.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
divRefs.forEach((ref, index) => {
|
||||
if (ref.current) {
|
||||
const slide = slideList[index];
|
||||
renderManagerService.getRenderById(slide.id)?.engine.setContainer(ref.current);
|
||||
}
|
||||
});
|
||||
|
||||
if (divRefs.length > 0) {
|
||||
commandService.syncExecuteCommand(SetSlidePageThumbOperation.id);
|
||||
}
|
||||
}, [divRefs]); // 依赖于divRefs数组的变化
|
||||
|
||||
const activatePage = useCallback((page: string) => {
|
||||
commandService.syncExecuteCommand(ActivateSlidePageOperation.id, { id: page });
|
||||
}, [commandService]);
|
||||
|
||||
return (
|
||||
<div className={styles.uniSlideBar} ref={slideBarRef}>
|
||||
<Scrollbar>
|
||||
<div className={styles.uniSlideBarContent}>
|
||||
{slideList.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={clsx(styles.uniSlideBarItem, {
|
||||
[styles.uniSlideBarItemActive]: item.id === activatePageId,
|
||||
})}
|
||||
onClick={() => activatePage(item.id)}
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
<div ref={divRefs[index]} className={styles.uniSlideBarBox} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Scrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
.uni-slide-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #eaecf0;
|
||||
|
||||
&-content {
|
||||
.uni-slide-bar-item {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
margin: 15px 0;
|
||||
width: 160px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ecedef;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
font-size: 12px;
|
||||
left: 6px;
|
||||
bottom: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(20, 34, 43, 0.4);
|
||||
z-index: 10;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.uni-slide-bar-box {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
&-active {
|
||||
border-color: rgb(var(--blue-500));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgb(var(--blue-500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uni-slide-add-button {
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
width: 45px;
|
||||
height: auto;
|
||||
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@univerjs/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@univerjs/shared/tsconfigs/node",
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import createViteConfig from '@univerjs/shared/vite';
|
||||
import pkg from './package.json';
|
||||
|
||||
export default ({ mode }) => createViteConfig({}, {
|
||||
mode,
|
||||
pkg,
|
||||
features: {
|
||||
react: true,
|
||||
css: true,
|
||||
dom: true,
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@univerjs/uniui",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.6",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"author": "DreamNum <developer@univer.ai>",
|
||||
@@ -62,7 +62,10 @@
|
||||
"peerDependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"clsx": ">=2.0.0",
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0",
|
||||
@@ -70,7 +73,9 @@
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.1",
|
||||
"@univerjs/icons": "^0.1.72",
|
||||
"@xyflow/react": "^12.0.4",
|
||||
"rc-util": "^5.43.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -78,8 +83,10 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/shared": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"less": "^4.2.0",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, IExecutionOptions } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { FlowManagerService } from '../../services/flow/flow-manager.service';
|
||||
|
||||
export interface ISetFlowViewportParams {
|
||||
viewport: {
|
||||
zoom: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
// This operation will not trigger viewport change locally but for live-share collaboration.
|
||||
|
||||
export const SetFlowViewportOperation: ICommand = {
|
||||
id: 'uniui.operation.set-flow-viewport',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor, params: ISetFlowViewportParams, options?: IExecutionOptions) => {
|
||||
const { fromCollab } = options || {};
|
||||
if (!fromCollab) {
|
||||
return true;
|
||||
}
|
||||
const flowManagerService = accessor.get(FlowManagerService);
|
||||
const oldViewport = flowManagerService.getViewport();
|
||||
const { viewport } = params;
|
||||
const { zoom, x, y } = viewport;
|
||||
if (oldViewport) {
|
||||
if (zoom === oldViewport.zoom && x === oldViewport.x && y === oldViewport.y) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
flowManagerService.setViewport(viewport);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 } from '@univerjs/core';
|
||||
import { CommandType, IUniverInstanceService } from '@univerjs/core';
|
||||
|
||||
export interface IUniFocusUnitParams {
|
||||
unitId: string | null;
|
||||
}
|
||||
|
||||
export const UniFocusUnitOperation: ICommand = {
|
||||
id: 'uniui.operation.uni-focus-unit',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor, params: IUniFocusUnitParams) => {
|
||||
const { unitId } = params;
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
instanceService.focusUnit(unitId);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 } from '@univerjs/core';
|
||||
import { CommandType, IUniverInstanceService } from '@univerjs/core';
|
||||
|
||||
export const DisposeUnitOperation: ICommand = {
|
||||
id: 'uniui.operation.dispose-unit',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const unitId = univerInstanceService.getFocusedUnit()?.getUnitId();
|
||||
if (unitId) {
|
||||
univerInstanceService.disposeUnit(unitId);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, IUniverInstanceService } from '@univerjs/core';
|
||||
import { type IMenuButtonItem, type IMenuItem, type IMenuSelectorItem, MenuGroup, MenuItemType, MenuPosition } from '@univerjs/ui';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { DisposeUnitOperation } from '../commands/operations/uni.operation';
|
||||
|
||||
export const UNIT_LINE_COLOR_MENU_ID = 'UNIT_LINE_COLOR_MENU_ID';
|
||||
export const DOWNLOAD_MENU_ID = 'DOWNLOAD_MENU_ID';
|
||||
export const SHARE_MENU_ID = 'SHARE_MENU_ID';
|
||||
export const LOCK_MENU_ID = 'LOCK_MENU_ID';
|
||||
export const PRINT_MENU_ID = 'PRINT_MENU_ID';
|
||||
export const ZEN_MENU_ID = 'ZEN_MENU_ID';
|
||||
export const DELETE_MENU_ID = DisposeUnitOperation.id;
|
||||
export const FRAME_SIZE_MENU_ID = 'FRAME_SIZE_MENU_ID';
|
||||
|
||||
export const FONT_GROUP_MENU_ID = 'FONT_GROUP_MENU_ID';
|
||||
|
||||
export const FAKE_FONT_FAMILY_MENU_ID = 'FAKE_FONT_FAMILY_MENU_ID';
|
||||
export const FAKE_FONT_SIZE_MENU_ID = 'FAKE_FONT_SIZE_MENU_ID';
|
||||
export const FAKE_FONT_COLOR_MENU_ID = 'FAKE_FONT_COLOR_MENU_ID';
|
||||
export const FAKE_BG_COLOR_MENU_ID = 'FAKE_BG_COLOR_MENU_ID';
|
||||
export const FAKE_IMAGE_MENU_ID = 'FAKE_IMAGE_MENU_ID';
|
||||
export const FAKE_FONT_GROUP_MENU_ID = 'FAKE_FONT_GROUP_MENU_ID';
|
||||
|
||||
export enum UNI_MENU_POSITIONS {
|
||||
TOOLBAR_MAIN = 'toolbar_main',
|
||||
TOOLBAR_FLOAT = 'toolbar_float',
|
||||
}
|
||||
|
||||
export function FakeFontFamilySelectorMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<string> {
|
||||
return {
|
||||
id: FAKE_FONT_FAMILY_MENU_ID,
|
||||
tooltip: 'toolbar.font',
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.SELECTOR,
|
||||
label: 'UI_PLUGIN_DOCS_FONT_FAMILY_COMPONENT',
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
selections: [],
|
||||
disabled$: new Observable((subscriber) => { subscriber.next(true); }),
|
||||
};
|
||||
}
|
||||
|
||||
export function FakeFontSizeSelectorMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<number> {
|
||||
return {
|
||||
id: FAKE_FONT_SIZE_MENU_ID,
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.SELECTOR,
|
||||
tooltip: 'toolbar.fontSize',
|
||||
label: {
|
||||
name: 'UI_PLUGIN_DOCS_FONT_SIZE_COMPONENT',
|
||||
props: {
|
||||
min: 1,
|
||||
max: 400,
|
||||
},
|
||||
},
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
selections: [],
|
||||
disabled$: new Observable((subscriber) => { subscriber.next(true); }),
|
||||
};
|
||||
}
|
||||
|
||||
export function FakeTextColorSelectorMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<string> {
|
||||
return {
|
||||
id: FAKE_FONT_COLOR_MENU_ID,
|
||||
icon: 'FontColor',
|
||||
tooltip: 'toolbar.textColor.main',
|
||||
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON_SELECTOR,
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
selections: [],
|
||||
disabled$: new Observable((subscriber) => { subscriber.next(true); }),
|
||||
};
|
||||
}
|
||||
|
||||
export function FakeBackgroundColorSelectorMenuItemFactory(accessor: IAccessor): IMenuSelectorItem<string> {
|
||||
return {
|
||||
id: FAKE_BG_COLOR_MENU_ID,
|
||||
tooltip: 'toolbar.fillColor.main',
|
||||
group: MenuGroup.TOOLBAR_FORMAT,
|
||||
type: MenuItemType.BUTTON_SELECTOR,
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
icon: 'PaintBucket',
|
||||
selections: [],
|
||||
disabled$: new Observable((subscriber) => { subscriber.next(true); }),
|
||||
};
|
||||
}
|
||||
|
||||
export function FakeImageMenuFactory(accessor: IAccessor): IMenuItem {
|
||||
return {
|
||||
id: FAKE_IMAGE_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
group: MenuGroup.TOOLBAR_FORMULAS_INSERT,
|
||||
icon: 'addition-and-subtraction-single',
|
||||
tooltip: 'sheetImage.title',
|
||||
disabled$: new Observable((subscriber) => { subscriber.next(true); }),
|
||||
};
|
||||
}
|
||||
|
||||
export function FontGroupMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
return {
|
||||
id: FONT_GROUP_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
tooltip: 'Font group',
|
||||
icon: 'BoldSingle',
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
hidden$: univerInstanceService.focused$.pipe(map((focused) => !focused)),
|
||||
};
|
||||
}
|
||||
|
||||
export function FakeFontGroupMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
|
||||
return {
|
||||
id: FAKE_FONT_GROUP_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
tooltip: 'Font group',
|
||||
icon: 'BoldSingle',
|
||||
positions: [MenuPosition.TOOLBAR_START],
|
||||
disabled$: new Observable((subscriber) => { subscriber.next(true); }),
|
||||
};
|
||||
}
|
||||
|
||||
export function UnitLineColorMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: UNIT_LINE_COLOR_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: '',
|
||||
icon: '',
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export function DownloadMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: DOWNLOAD_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
group: MenuGroup.TOOLBAR_OTHERS,
|
||||
title: '',
|
||||
tooltip: 'Download',
|
||||
icon: 'DownloadSingle',
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function ShareMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: SHARE_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
group: MenuGroup.TOOLBAR_OTHERS,
|
||||
title: '',
|
||||
tooltip: 'Share',
|
||||
icon: 'ShareSingle',
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export function LockMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: LOCK_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
group: MenuGroup.TOOLBAR_OTHERS,
|
||||
title: '',
|
||||
tooltip: 'Lock',
|
||||
icon: 'LockSingle',
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function PrintMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: PRINT_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
group: MenuGroup.TOOLBAR_OTHERS,
|
||||
title: '',
|
||||
tooltip: 'Print',
|
||||
icon: 'PrintSingle',
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export function ZenMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: ZEN_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: '',
|
||||
icon: 'ZenSingle',
|
||||
tooltip: 'Full screen',
|
||||
group: MenuGroup.TOOLBAR_OTHERS,
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function DeleteMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
|
||||
return {
|
||||
id: DELETE_MENU_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'Delete',
|
||||
tooltip: 'Delete',
|
||||
icon: 'DeleteSingle',
|
||||
group: MenuGroup.TOOLBAR_OTHERS,
|
||||
positions: [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { connectInjector, Disposable, Inject, Injector, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core';
|
||||
import { connectInjector, Disposable, Inject, Injector, LifecycleService, LifecycleStages, Optional, toDisposable } from '@univerjs/core';
|
||||
import type { IUniverUIConfig, IWorkbenchOptions } from '@univerjs/ui';
|
||||
import { BuiltInUIPart, CanvasPopup, FloatDom, IUIPartsService } from '@univerjs/ui';
|
||||
import type { IDisposable } from '@univerjs/core';
|
||||
import { BuiltInUIPart, CanvasPopup, FloatDom, ILayoutService, IUIPartsService } from '@univerjs/ui';
|
||||
import React from 'react';
|
||||
import { delay, filter, take } from 'rxjs';
|
||||
import { render as createRoot, unmount } from 'rc-util/lib/React/render';
|
||||
@@ -31,7 +31,8 @@ export class UniverUniUIController extends Disposable {
|
||||
private readonly _config: IUniverUIConfig,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@Inject(LifecycleService) private readonly _lifecycleService: LifecycleService,
|
||||
@IUIPartsService private readonly _uiPartsService: IUIPartsService
|
||||
@IUIPartsService private readonly _uiPartsService: IUIPartsService,
|
||||
@Optional(ILayoutService) private readonly _layoutService?: ILayoutService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -42,7 +43,12 @@ export class UniverUniUIController extends Disposable {
|
||||
|
||||
private _bootstrapWorkbench(): void {
|
||||
this.disposeWithMe(
|
||||
bootstrap(this._injector, this._config, () => {
|
||||
bootstrap(this._injector, this._config, (contentEl, containerEl) => {
|
||||
if (this._layoutService) {
|
||||
this.disposeWithMe(this._layoutService.registerContentElement(contentEl));
|
||||
this.disposeWithMe(this._layoutService.registerContainerElement(containerEl));
|
||||
}
|
||||
|
||||
this._lifecycleService.lifecycle$.pipe(
|
||||
filter((lifecycle) => lifecycle === LifecycleStages.Ready),
|
||||
delay(300),
|
||||
@@ -64,7 +70,7 @@ export class UniverUniUIController extends Disposable {
|
||||
function bootstrap(
|
||||
injector: Injector,
|
||||
options: IWorkbenchOptions,
|
||||
callback: () => void
|
||||
callback: (contentEl: HTMLElement, containerElement: HTMLElement) => void
|
||||
): IDisposable {
|
||||
let mountContainer: HTMLElement;
|
||||
|
||||
@@ -88,7 +94,7 @@ function bootstrap(
|
||||
<ConnectedApp
|
||||
{...options}
|
||||
mountContainer={mountContainer}
|
||||
onRendered={callback}
|
||||
onRendered={(contentEl) => callback(contentEl, mountContainer)}
|
||||
/>,
|
||||
mountContainer
|
||||
);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, LifecycleStages, OnLifecycle } from '@univerjs/core';
|
||||
import { auditTime, throttleTime } from 'rxjs';
|
||||
import { SetFlowViewportOperation } from '../commands/operations/set-flow-viewport.operation';
|
||||
import type { IFlowViewport } from '../services/flow/flow-manager.service';
|
||||
import { FlowManagerService } from '../services/flow/flow-manager.service';
|
||||
import { UniFocusUnitOperation } from '../commands/operations/uni-focus-unit.operation';
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, UniuiFlowController)
|
||||
export class UniuiFlowController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService protected readonly _commandService: ICommandService,
|
||||
@Inject(FlowManagerService) protected readonly _flowManagerService: FlowManagerService
|
||||
) {
|
||||
super();
|
||||
this._initCommands();
|
||||
this._triggerCommands();
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
SetFlowViewportOperation,
|
||||
UniFocusUnitOperation,
|
||||
].forEach((c) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(c));
|
||||
});
|
||||
}
|
||||
|
||||
private _triggerCommands() {
|
||||
this._flowManagerService.viewportChanged$.pipe(
|
||||
throttleTime(100, undefined, { leading: true, trailing: true }),
|
||||
auditTime(100)
|
||||
).subscribe((viewport: IFlowViewport | null) => {
|
||||
if (viewport) {
|
||||
this._commandService.executeCommand(SetFlowViewportOperation.id, { viewport });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core';
|
||||
import type { ISidebarService } from '@univerjs/ui';
|
||||
import { ComponentManager, ILeftSidebarService } from '@univerjs/ui';
|
||||
import { distinctUntilChanged, map } from 'rxjs';
|
||||
import { Outline } from '../views/outline/Outline';
|
||||
|
||||
const OUTLINE_COMPONENT = 'OUTLINE_COMPONENT';
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, UniuiLeftSidebarController)
|
||||
export class UniuiLeftSidebarController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ILeftSidebarService private readonly _leftSidebarService: ISidebarService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initComponents();
|
||||
this._initShowOutlineListener();
|
||||
}
|
||||
|
||||
private _initComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(OUTLINE_COMPONENT, Outline));
|
||||
}
|
||||
|
||||
private _initShowOutlineListener(): void {
|
||||
this.disposeWithMe(this._univerInstanceService.focused$.pipe(
|
||||
map((unitId) => !unitId ? false : this._univerInstanceService.getUnit(unitId)?.type !== UniverInstanceType.UNIVER_DOC),
|
||||
distinctUntilChanged()
|
||||
).subscribe((showOutline) => {
|
||||
if (showOutline) {
|
||||
this._leftSidebarService.open({
|
||||
children: { label: OUTLINE_COMPONENT },
|
||||
visible: true,
|
||||
width: 176,
|
||||
});
|
||||
} else {
|
||||
this._leftSidebarService.close();
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._univerInstanceService.unitDisposed$.pipe(
|
||||
distinctUntilChanged()
|
||||
).subscribe(() => {
|
||||
this._leftSidebarService.close();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, Injector, LifecycleStages, OnLifecycle } from '@univerjs/core';
|
||||
import { DeleteSingle, DownloadSingle, LockSingle, PrintSingle, ShareSingle, ZenSingle } from '@univerjs/icons';
|
||||
import type { IMenuItemFactory, MenuConfig } from '@univerjs/ui';
|
||||
import { ComponentManager, IMenuService } from '@univerjs/ui';
|
||||
import { DisposeUnitOperation } from '../commands/operations/uni.operation';
|
||||
import { UniToolbarService } from '../services/toolbar/uni-toolbar-service';
|
||||
import { DeleteMenuItemFactory, DownloadMenuItemFactory, FakeBackgroundColorSelectorMenuItemFactory, FakeFontFamilySelectorMenuItemFactory, FakeFontGroupMenuItemFactory, FakeFontSizeSelectorMenuItemFactory, FakeImageMenuFactory, FakeTextColorSelectorMenuItemFactory, FontGroupMenuItemFactory, LockMenuItemFactory, PrintMenuItemFactory, ShareMenuItemFactory, ZenMenuItemFactory } from './menu';
|
||||
|
||||
export interface IUniuiToolbarConfig {
|
||||
menu: MenuConfig;
|
||||
}
|
||||
|
||||
export const DefaultUniuiToolbarConfig = {};
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, UniuiToolbarController)
|
||||
export class UniuiToolbarController extends Disposable {
|
||||
constructor(
|
||||
@IMenuService protected readonly _menuService: IMenuService,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@Inject(ComponentManager) protected readonly _componentManager: ComponentManager,
|
||||
@ICommandService protected readonly _commandService: ICommandService,
|
||||
@Inject(UniToolbarService) protected readonly _toolbarService: UniToolbarService
|
||||
) {
|
||||
super();
|
||||
this._initComponent();
|
||||
this._initMenus();
|
||||
this._initCommands();
|
||||
}
|
||||
|
||||
private _initComponent(): void {
|
||||
const componentManager = this._componentManager;
|
||||
const iconList: Record<string, React.ForwardRefExoticComponent<any>> = {
|
||||
DownloadSingle,
|
||||
ShareSingle,
|
||||
LockSingle,
|
||||
PrintSingle,
|
||||
ZenSingle,
|
||||
DeleteSingle,
|
||||
};
|
||||
for (const k in iconList) {
|
||||
this.disposeWithMe(componentManager.register(k, iconList[k]));
|
||||
}
|
||||
}
|
||||
|
||||
private _initMenus(): void {
|
||||
// register menu factories
|
||||
(
|
||||
[
|
||||
DownloadMenuItemFactory,
|
||||
ShareMenuItemFactory,
|
||||
LockMenuItemFactory,
|
||||
PrintMenuItemFactory,
|
||||
ZenMenuItemFactory,
|
||||
DeleteMenuItemFactory,
|
||||
FontGroupMenuItemFactory,
|
||||
FakeFontFamilySelectorMenuItemFactory,
|
||||
FakeTextColorSelectorMenuItemFactory,
|
||||
FakeFontSizeSelectorMenuItemFactory,
|
||||
FakeBackgroundColorSelectorMenuItemFactory,
|
||||
FakeImageMenuFactory,
|
||||
FakeFontGroupMenuItemFactory,
|
||||
] as IMenuItemFactory[]
|
||||
).forEach((factory) => {
|
||||
this.disposeWithMe(this._menuService.addMenuItem(this._injector.invoke(factory), {}));
|
||||
});
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
DisposeUnitOperation,
|
||||
].forEach((c) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(c));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { Tools } from '@univerjs/core';
|
||||
|
||||
export const generateCloneMutation = (cloneId: string, sourceMutation: ICommand): ICommand => {
|
||||
return {
|
||||
...Tools.deepClone(sourceMutation),
|
||||
id: cloneId,
|
||||
};
|
||||
};
|
||||
@@ -15,3 +15,12 @@
|
||||
*/
|
||||
|
||||
export { UniverUniUIPlugin } from './plugin';
|
||||
export { UniUIPart } from './views/outline/Outline';
|
||||
export { getGridUnitLocalCacheKey, UnitGridService, IUnitGridService, type IProjectNode, type IUnitGrid } from './services/unit-grid/unit-grid.service';
|
||||
export { UniFloatToolbarUIPart } from './views/uni-toolbar/UniFloatToolbar';
|
||||
export { SetFlowViewportOperation } from './commands/operations/set-flow-viewport.operation';
|
||||
export { UniFocusUnitOperation } from './commands/operations/uni-focus-unit.operation';
|
||||
export { FONT_GROUP_MENU_ID, UNI_MENU_POSITIONS, ZEN_MENU_ID, LOCK_MENU_ID, PRINT_MENU_ID, SHARE_MENU_ID, DELETE_MENU_ID, DOWNLOAD_MENU_ID } from './controllers/menu';
|
||||
export { UniToolbarService } from './services/toolbar/uni-toolbar-service';
|
||||
export { BuiltinUniToolbarItemId } from './services/toolbar/uni-toolbar-service';
|
||||
export { generateCloneMutation } from './controllers/utils';
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
IEditorService,
|
||||
IGlobalZoneService,
|
||||
ILayoutService,
|
||||
ILeftSidebarService,
|
||||
IMenuService,
|
||||
IMessageService,
|
||||
INotificationService,
|
||||
@@ -69,8 +70,13 @@ import {
|
||||
UNIVER_UI_PLUGIN_NAME,
|
||||
ZIndexManager,
|
||||
} from '@univerjs/ui';
|
||||
import { FlowManagerService } from './services/flow/flow-manager.service';
|
||||
import { UniverUniUIController } from './controllers/uniui-desktop.controller';
|
||||
import { UnitGridService } from './services/unit-grid/unit-grid.service';
|
||||
import { IUnitGridService, UnitGridService } from './services/unit-grid/unit-grid.service';
|
||||
import { UniuiLeftSidebarController } from './controllers/uniui-leftsidebar.controller';
|
||||
import { UniuiToolbarController } from './controllers/uniui-toolbar.controller';
|
||||
import { UniuiFlowController } from './controllers/uniui-flow.controller';
|
||||
import { UniToolbarService } from './services/toolbar/uni-toolbar-service';
|
||||
|
||||
const UI_BOOTSTRAP_DELAY = 16;
|
||||
|
||||
@@ -100,7 +106,9 @@ export class UniverUniUIPlugin extends Plugin {
|
||||
[ComponentManager],
|
||||
[ZIndexManager],
|
||||
[ShortcutPanelService],
|
||||
[UnitGridService],
|
||||
[FlowManagerService],
|
||||
[UniToolbarService],
|
||||
[IUnitGridService, { useClass: UnitGridService }],
|
||||
[IUIPartsService, { useClass: UIPartsService }],
|
||||
[ILayoutService, { useClass: DesktopLayoutService }],
|
||||
[IShortcutService, { useClass: ShortcutService }],
|
||||
@@ -112,6 +120,7 @@ export class UniverUniUIPlugin extends Plugin {
|
||||
[IDialogService, { useClass: DesktopDialogService, lazy: true }],
|
||||
[IConfirmService, { useClass: DesktopConfirmService, lazy: true }],
|
||||
[ISidebarService, { useClass: DesktopSidebarService, lazy: true }],
|
||||
[ILeftSidebarService, { useClass: DesktopSidebarService, lazy: true }],
|
||||
[IZenZoneService, { useClass: DesktopZenZoneService, lazy: true }],
|
||||
[IGlobalZoneService, { useClass: DesktopGlobalZoneService, lazy: true }],
|
||||
[IMessageService, { useClass: DesktopMessageService, lazy: true }],
|
||||
@@ -132,6 +141,9 @@ export class UniverUniUIPlugin extends Plugin {
|
||||
[ShortcutPanelController, {
|
||||
useFactory: () => this._injector.createInstance(ShortcutPanelController, this._config),
|
||||
}],
|
||||
[UniuiLeftSidebarController],
|
||||
[UniuiToolbarController],
|
||||
[UniuiFlowController],
|
||||
], this._config.override);
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, LifecycleStages, OnLifecycle } from '@univerjs/core';
|
||||
import type {
|
||||
ReactFlowInstance,
|
||||
} from '@xyflow/react';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface IFlowViewport {
|
||||
zoom: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
@OnLifecycle(LifecycleStages.Ready, FlowManagerService)
|
||||
export class FlowManagerService extends Disposable {
|
||||
private readonly _viewportChanged$ = new BehaviorSubject<IFlowViewport | null>(null);
|
||||
readonly viewportChanged$ = this._viewportChanged$.asObservable();
|
||||
|
||||
private _flowInstance: ReactFlowInstance<any>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getViewport() {
|
||||
return this._flowInstance?.getViewport();
|
||||
}
|
||||
|
||||
setViewport(viewport: { zoom: number; x: number; y: number }) {
|
||||
this._flowInstance?.setViewport(viewport, {
|
||||
duration: 100,
|
||||
});
|
||||
}
|
||||
|
||||
setReactFlowInstance(instance: ReactFlowInstance<any>) {
|
||||
this._flowInstance = instance;
|
||||
}
|
||||
|
||||
fitView() {
|
||||
if (this._flowInstance) {
|
||||
this._flowInstance.fitView();
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
if (this._flowInstance) {
|
||||
this._flowInstance.zoomIn();
|
||||
}
|
||||
}
|
||||
|
||||
zoomOut() {
|
||||
if (this._flowInstance) {
|
||||
this._flowInstance.zoomOut();
|
||||
}
|
||||
}
|
||||
|
||||
setViewportChanged(viewport: IFlowViewport) {
|
||||
this._viewportChanged$.next(viewport);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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, RedoCommand, toDisposable, UndoCommand, UniverInstanceType } from '@univerjs/core';
|
||||
import { FAKE_BG_COLOR_MENU_ID, FAKE_FONT_COLOR_MENU_ID, FAKE_FONT_FAMILY_MENU_ID, FAKE_FONT_GROUP_MENU_ID, FAKE_FONT_SIZE_MENU_ID, FAKE_IMAGE_MENU_ID, FONT_GROUP_MENU_ID } from '../../controllers/menu';
|
||||
|
||||
interface IItemImpl {
|
||||
id: string;
|
||||
type: UniverInstanceType;
|
||||
}
|
||||
|
||||
export interface IUniToolbarItem {
|
||||
id: string;
|
||||
impl: IItemImpl[];
|
||||
}
|
||||
|
||||
export enum BuiltinUniToolbarItemId {
|
||||
UNDO = 'undo',
|
||||
REDO = 'redo',
|
||||
FONT_FAMILY = 'font-famaily',
|
||||
FONT_SIZE = 'font-size',
|
||||
FONT_GROUP = 'font-group',
|
||||
COLOR = 'color',
|
||||
BACKGROUND = 'background',
|
||||
IMAGE = 'image',
|
||||
PIVOT_TABLE = 'pivot-table',
|
||||
}
|
||||
|
||||
export class UniToolbarService extends Disposable {
|
||||
private _items: IUniToolbarItem[];
|
||||
constructor() {
|
||||
super();
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._items = [
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.UNDO,
|
||||
impl: [{ id: UndoCommand.id, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.REDO,
|
||||
impl: [{ id: RedoCommand.id, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.FONT_FAMILY,
|
||||
impl: [{ id: FAKE_FONT_FAMILY_MENU_ID, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.FONT_SIZE,
|
||||
impl: [{ id: FAKE_FONT_SIZE_MENU_ID, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.FONT_GROUP,
|
||||
impl: [
|
||||
{ id: FAKE_FONT_GROUP_MENU_ID, type: UniverInstanceType.UNIVER_UNKNOWN },
|
||||
{ id: FONT_GROUP_MENU_ID, type: UniverInstanceType.UNIVER_SHEET },
|
||||
{ id: FONT_GROUP_MENU_ID, type: UniverInstanceType.UNIVER_DOC },
|
||||
{ id: FONT_GROUP_MENU_ID, type: UniverInstanceType.UNIVER_SLIDE },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.COLOR,
|
||||
impl: [{ id: FAKE_FONT_COLOR_MENU_ID, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.BACKGROUND,
|
||||
impl: [{ id: FAKE_BG_COLOR_MENU_ID, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
{
|
||||
id: BuiltinUniToolbarItemId.IMAGE,
|
||||
impl: [{ id: FAKE_IMAGE_MENU_ID, type: UniverInstanceType.UNIVER_UNKNOWN }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
addItem(item: IUniToolbarItem) {
|
||||
this._items.push(item);
|
||||
return toDisposable(() => {
|
||||
this._items = this._items.filter((i) => i.id !== item.id);
|
||||
});
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
implementItem(id: string, impl: IItemImpl) {
|
||||
const item = this._items.find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.impl.push(impl);
|
||||
return toDisposable(() => {
|
||||
item.impl = item.impl.filter((i) => i.id !== impl.id);
|
||||
});
|
||||
}
|
||||
return toDisposable(() => {});
|
||||
}
|
||||
}
|
||||
@@ -14,59 +14,177 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, isInternalEditorID } from '@univerjs/core';
|
||||
import type { Nullable, UnitModel, UniverInstanceType } from '@univerjs/core';
|
||||
import { createIdentifier, Disposable, ILocalStorageService, isInternalEditorID, IUniverInstanceService } from '@univerjs/core';
|
||||
import type { IRender } from '@univerjs/engine-render';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export type IUnitGrid = string[];
|
||||
export type IUnitGrid = IProjectNode[];
|
||||
|
||||
export interface IProjectNode {
|
||||
id: string;
|
||||
data: {
|
||||
unitId: string;
|
||||
type: UniverInstanceType;
|
||||
};
|
||||
style: {
|
||||
width: string;
|
||||
height: string;
|
||||
display: string;
|
||||
borderRadius: string;
|
||||
border: string;
|
||||
backgroundColor: string;
|
||||
};
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IUnitGridService {
|
||||
readonly unitGrid: IProjectNode[];
|
||||
readonly unitGrid$: Observable<IProjectNode[]>;
|
||||
readonly newNode: IProjectNode | null;
|
||||
readonly newNode$: Observable<IProjectNode | null>;
|
||||
|
||||
setContainerForRender(unitId: string, element: HTMLElement): void;
|
||||
|
||||
changeDimension(id: string, dimensions: { width: number; height: number }): void;
|
||||
changePosition(id: string, position: { x: number; y: number }): void;
|
||||
|
||||
}
|
||||
|
||||
export const IUnitGridService = createIdentifier<IUnitGridService>('uniui.grid.service');
|
||||
|
||||
// NOTE: please note that caching position and dimension of units is just for demo.
|
||||
// The grid should be a unit itself.
|
||||
|
||||
/**
|
||||
* This services decides which (now at maximum two) units are going to be rendered in the browser.
|
||||
*/
|
||||
export class UnitGridService extends Disposable {
|
||||
export class UnitGridService extends Disposable implements IUnitGridService {
|
||||
// TODO@wzhudev: currently we only support at maximum 2 units rendered side by side.
|
||||
// In the future we would introduce a grid system (very much like vscode's) to support more
|
||||
// units and more complex layout.
|
||||
private _unitGrid: IUnitGrid = [];
|
||||
protected _unitGrid: IUnitGrid = [];
|
||||
private readonly _unitGrid$ = new BehaviorSubject<IUnitGrid>(this._unitGrid);
|
||||
readonly unitGrid$ = this._unitGrid$.asObservable();
|
||||
|
||||
protected _newNode: IProjectNode | null = null;
|
||||
private readonly _newNode$ = new BehaviorSubject<IProjectNode | null>(null);
|
||||
readonly newNode$ = this._newNode$.asObservable();
|
||||
|
||||
get unitGrid() { return this._unitGrid; }
|
||||
get cachedGrid() { return this._cachedGrid; }
|
||||
get newNode() { return this._newNode; }
|
||||
|
||||
private _nodeIndex = 0;
|
||||
|
||||
constructor(
|
||||
@IRenderManagerService private readonly _renderSrv: IRenderManagerService
|
||||
@IRenderManagerService protected readonly _renderSrv: IRenderManagerService,
|
||||
@ILocalStorageService protected readonly _localStorageService: ILocalStorageService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
setContainerForRender(unitId: string, element: HTMLElement) {
|
||||
this._renderSrv.getRenderById(unitId)?.engine.setContainer(element);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._unitGrid$.complete();
|
||||
this._newNode$.complete();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
setNewNodeObserver() {
|
||||
this._newNode$.next(this._newNode);
|
||||
}
|
||||
|
||||
setContainerForRender(unitId: string, element: HTMLElement) {
|
||||
this._renderSrv.getRenderById(unitId)?.engine.setContainer(element);
|
||||
}
|
||||
|
||||
changeDimension(id: string, dimension: { width: number; height: number }): void {
|
||||
const node = this._unitGrid.find((item) => item.id === id);
|
||||
if (node) {
|
||||
node.style.width = `${dimension.width}px`;
|
||||
node.style.height = `${dimension.height}px`;
|
||||
this._cacheData();
|
||||
}
|
||||
}
|
||||
|
||||
changePosition(id: string, position: { x: number; y: number }): void {
|
||||
const node = this._unitGrid.find((item) => item.id === id);
|
||||
if (node) {
|
||||
node.position.x = position.x;
|
||||
node.position.y = position.y;
|
||||
this._cacheData();
|
||||
}
|
||||
}
|
||||
|
||||
private async _init(): Promise<void> {
|
||||
await this._initData();
|
||||
|
||||
this._renderSrv.getRenderAll().forEach((renderer) => this._onRendererCreated(renderer));
|
||||
this.disposeWithMe(this._renderSrv.created$.subscribe((renderer) => this._onRendererCreated(renderer)));
|
||||
this.disposeWithMe(this._renderSrv.disposed$.subscribe((unitId) => this._onRenderedDisposed(unitId)));
|
||||
this.disposeWithMe(this._renderSrv.created$.subscribe((render) => this._onRendererCreated(render)));
|
||||
// When hot reload in development mode, render dispose will be triggered, causing data to be cleared. Replace it with unitDispose
|
||||
this.disposeWithMe(this._univerInstanceService.unitDisposed$.subscribe((unitModel) => this._onUnitDisposed(unitModel)));
|
||||
}
|
||||
|
||||
private _onRendererCreated(renderer: IRender): void {
|
||||
if (isInternalEditorID(renderer.unitId)) {
|
||||
protected _cachedGrid: Nullable<IUnitGrid> = null;
|
||||
protected async _initData(): Promise<void> {
|
||||
const raw = await this._localStorageService.getItem<IUnitGrid>(getGridUnitLocalCacheKey('static'));
|
||||
if (raw) {
|
||||
this._cachedGrid = raw;
|
||||
}
|
||||
}
|
||||
|
||||
protected _cacheData(): void {
|
||||
this._localStorageService.setItem(getGridUnitLocalCacheKey('static'), this.unitGrid);
|
||||
}
|
||||
|
||||
protected _onRendererCreated(renderer: IRender): void {
|
||||
const { unitId, type, isThumbNail } = renderer;
|
||||
if (isInternalEditorID(unitId) || isThumbNail) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._unitGrid.push(renderer.unitId);
|
||||
this._insertNewNode(unitId, type);
|
||||
}
|
||||
|
||||
private _insertNewNode(unitId: string, type: UniverInstanceType): void {
|
||||
const index = this._nodeIndex;
|
||||
this._nodeIndex += 1;
|
||||
|
||||
const MAX_COUNT_IN_ROW = 3;
|
||||
const newNode: IProjectNode = this._cachedGrid?.find((item) => item.id === unitId) ?? {
|
||||
id: unitId,
|
||||
data: {
|
||||
unitId,
|
||||
type,
|
||||
},
|
||||
style: {
|
||||
width: '660px',
|
||||
height: '600px',
|
||||
display: 'flex',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
position: { x: (index % MAX_COUNT_IN_ROW) * 750, y: Math.floor(index / MAX_COUNT_IN_ROW) * 750 + 40 },
|
||||
};
|
||||
|
||||
this._unitGrid.push(newNode);
|
||||
this._newNode = newNode; // set new node, for viewport to scroll to
|
||||
this._emitLayoutChange();
|
||||
}
|
||||
|
||||
private _onRenderedDisposed(unitId: string): void {
|
||||
const idx = this._unitGrid.indexOf(unitId);
|
||||
protected _onUnitDisposed(unitModel: UnitModel): void {
|
||||
const unitId = unitModel.getUnitId();
|
||||
const idx = this._unitGrid.findIndex((item) => item.id === unitId);
|
||||
if (idx !== -1) {
|
||||
this._unitGrid.splice(idx, 1);
|
||||
this._emitLayoutChange();
|
||||
@@ -74,6 +192,11 @@ export class UnitGridService extends Disposable {
|
||||
}
|
||||
|
||||
private _emitLayoutChange(): void {
|
||||
this._unitGrid$.next(this._unitGrid);
|
||||
this._unitGrid$.next(this._unitGrid.slice());
|
||||
this._cacheData();
|
||||
}
|
||||
}
|
||||
|
||||
export function getGridUnitLocalCacheKey(projectId: string): string {
|
||||
return `project-cache-${projectId}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { IUniverInstanceService, useDependency } from '@univerjs/core';
|
||||
import { useObservable } from '@univerjs/ui';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useUnitTitle(unitId: string): string | undefined {
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
const unit = useMemo(() => instanceService.getUnit(unitId), [unitId, instanceService]);
|
||||
const title = useObservable<string>(unit?.name$, '', false, [unit]);
|
||||
return title;
|
||||
}
|
||||
|
||||
export function useUnitFocused(unitId: string): boolean {
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
const focusedUnitId = useObservable<Nullable<string>>(instanceService.focused$);
|
||||
return unitId === focusedUnitId;
|
||||
}
|
||||
+14
-12
@@ -14,18 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IUniverInstanceService } from '@univerjs/core';
|
||||
import { ComponentContainer, useComponentsOfPart } from '@univerjs/ui';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* This controller is responsible for registering formulas into `UniFormulaService`,
|
||||
* when a doc is loaded or a new formula is created. It is also responsible for un-registering
|
||||
* formula at current time.
|
||||
*/
|
||||
export class UniFormulaController {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceSrv: IUniverInstanceService
|
||||
) {
|
||||
|
||||
}
|
||||
export enum UniUIPart {
|
||||
OUTLINE = 'OUTLINE',
|
||||
}
|
||||
|
||||
export function Outline() {
|
||||
const outlineComponent = useComponentsOfPart(UniUIPart.OUTLINE);
|
||||
return (
|
||||
<div>
|
||||
<ComponentContainer key="outline" components={outlineComponent} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Outline.componentName = 'Outline';
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 {
|
||||
useReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import React, { useCallback, useEffect, useLayoutEffect } from 'react';
|
||||
import { CheckMarkSingle, FullscreenSingle, IncreaseSingle, ZoomReduceSingle } from '@univerjs/icons';
|
||||
import { Dropdown, Tooltip } from '@univerjs/design';
|
||||
import clsx from 'clsx';
|
||||
import { useDependency } from '@univerjs/core';
|
||||
import { UniDiv } from '../uni-toolbar/UniFloatToolbar';
|
||||
import type { IProjectNode } from '../../services/unit-grid/unit-grid.service';
|
||||
import { IUnitGridService } from '../../services/unit-grid/unit-grid.service';
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const UniControlButton = (props: { tooltips: string; children?: React.ReactElement; onClick: () => void; style?: React.CSSProperties }) => {
|
||||
const { children, onClick, style, tooltips } = props;
|
||||
return (
|
||||
<Tooltip title={tooltips}>
|
||||
<div className={styles.uniControlButton} onClick={onClick} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export const MAX_ZOOM = 2;
|
||||
export const MIN_ZOOM = 0.5;
|
||||
export const DEFAULT_ZOOM = 1;
|
||||
|
||||
// 0 means fit view
|
||||
const shortcuts = [50, 75, 100, 125, 150, 175, 200, 0];
|
||||
|
||||
export enum UniControlItem {
|
||||
AI = 'AI',
|
||||
}
|
||||
|
||||
export const UniControls = ({ zoom, onItemClick }: { zoom: number; onItemClick?: (key: UniControlItem) => void }) => {
|
||||
const zoomPercent = Math.floor(zoom * 100);
|
||||
const { zoomIn, zoomOut, fitView, setViewport, getNodes, setCenter, getZoom } = useReactFlow();
|
||||
const unitGridService = useDependency(IUnitGridService);
|
||||
|
||||
const onZoomInHandler = () => {
|
||||
zoomIn();
|
||||
};
|
||||
const onZoomOutHandler = () => {
|
||||
zoomOut();
|
||||
};
|
||||
|
||||
const onFullscreenHandler = () => {
|
||||
document.body.requestFullscreen();
|
||||
};
|
||||
|
||||
const setZoomAtCenter = useCallback((zoom: number) => {
|
||||
const nodes = getNodes();
|
||||
const xLeftPositions = nodes.map((node) => node.position.x);
|
||||
const xRightPositions = nodes.map((node) => node.position.x + (node.measured?.width ?? 0));
|
||||
const yTopPositions = nodes.map((node) => node.position.y);
|
||||
const yDownPositions = nodes.map((node) => node.position.y + (node.measured?.height ?? 0));
|
||||
const centerX = (Math.min(...xLeftPositions) + Math.max(...xRightPositions)) / 2;
|
||||
const centerY = (Math.min(...yTopPositions) + Math.max(...yDownPositions)) / 2;
|
||||
setCenter(centerX, centerY, { zoom });
|
||||
}, [getNodes, setCenter]);
|
||||
|
||||
const handleFitView = (node: IProjectNode | null) => {
|
||||
if (node) {
|
||||
const { x, y } = node.position;
|
||||
const width = Number.parseInt(`${node.style?.width}`) || 0;
|
||||
const height = Number.parseInt(`${node.style?.height}`) || 0;
|
||||
|
||||
// Use the setCenter method to position the viewport to the center of the node.
|
||||
setCenter(x + width / 2, y + height / 2, {
|
||||
duration: 300,
|
||||
zoom: getZoom(),
|
||||
});
|
||||
}
|
||||
};
|
||||
const onZoomMenuChange = useCallback((value: number) => {
|
||||
if (value === 0) {
|
||||
fitView();
|
||||
} else {
|
||||
setZoomAtCenter(value / 100);
|
||||
}
|
||||
}, [setZoomAtCenter, fitView]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setTimeout(() => {
|
||||
setZoomAtCenter(1);
|
||||
}, 1000);
|
||||
}, [setZoomAtCenter]);
|
||||
|
||||
useEffect(() => {
|
||||
const newNodeSubscribe = unitGridService.newNode$.subscribe((node) => handleFitView(node));
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
fitView();
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
newNodeSubscribe.unsubscribe();
|
||||
};
|
||||
}, [fitView]);
|
||||
|
||||
return (
|
||||
<div className={styles.uniControls}>
|
||||
<UniControlButton tooltips="Full screen" onClick={onFullscreenHandler}>
|
||||
<FullscreenSingle />
|
||||
</UniControlButton>
|
||||
<UniControlButton tooltips="Zoom in" onClick={onZoomInHandler}>
|
||||
<IncreaseSingle />
|
||||
</UniControlButton>
|
||||
<Dropdown
|
||||
placement="topLeft"
|
||||
overlay={(
|
||||
<div className={styles.sliderShortcuts}>
|
||||
{shortcuts?.map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
className={clsx(
|
||||
styles.sliderShortcut,
|
||||
item === zoomPercent ? styles.sliderShortcutActive : ''
|
||||
)}
|
||||
onClick={() => onZoomMenuChange(item)}
|
||||
>
|
||||
{item === zoomPercent && (
|
||||
<span className={styles.sliderShortcutIcon}>
|
||||
<CheckMarkSingle />
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{item === 0 ? 'Fit View' : `${item}%`}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<a className={styles.sliderValue}>
|
||||
{zoomPercent}
|
||||
%
|
||||
</a>
|
||||
</Dropdown>
|
||||
<UniControlButton tooltips="Zoom out" onClick={onZoomOutHandler}>
|
||||
<ZoomReduceSingle />
|
||||
</UniControlButton>
|
||||
<UniDiv />
|
||||
<UniControlButton tooltips="AI" onClick={() => onItemClick?.(UniControlItem.AI)} style={{ background: '#274FEE' }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M5.09091 19C4.63904 19 4.27273 18.6337 4.27273 18.1818C4.27273 17.7299 4.63904 17.3636 5.09091 17.3636C5.54278 17.3636 5.90909 17.7299 5.90909 18.1818C5.90909 18.6337 5.54278 19 5.09091 19Z" fill="white" />
|
||||
<path d="M14.9091 2.63636C14.4572 2.63636 14.0909 2.27005 14.0909 1.81818C14.0909 1.36631 14.4572 1 14.9091 1C15.361 1 15.7273 1.36631 15.7273 1.81818C15.7273 2.27005 15.361 2.63636 14.9091 2.63636Z" fill="white" />
|
||||
<path d="M18.1818 5.90909C17.7299 5.90909 17.3636 5.54278 17.3636 5.09091C17.3636 4.63904 17.7299 4.27273 18.1818 4.27273C18.6337 4.27273 19 4.63904 19 5.09091C19 5.54278 18.6337 5.90909 18.1818 5.90909Z" fill="white" />
|
||||
<path d="M8.93208 4.29446C9.20363 3.17457 10.7964 3.17457 11.0679 4.29446L11.8151 7.3759C11.9121 7.77574 12.2243 8.08792 12.6241 8.18487L15.7055 8.93208C16.8254 9.20363 16.8254 10.7964 15.7055 11.0679L12.6241 11.8151C12.2243 11.9121 11.9121 12.2243 11.8151 12.6241L11.0679 15.7055C10.7964 16.8254 9.20363 16.8254 8.93208 15.7055L8.18487 12.6241C8.08792 12.2243 7.77574 11.9121 7.3759 11.8151L4.29446 11.0679C3.17457 10.7964 3.17457 9.20363 4.29446 8.93208L7.3759 8.18487C7.77574 8.08792 8.08792 7.77574 8.18487 7.3759L8.93208 4.29446Z" fill="white" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M18.4393 12.4507C18.8203 12.5929 19.0139 13.0171 18.8717 13.3981C17.9506 15.8661 16.0383 17.8485 13.6187 18.8611C13.2435 19.0181 12.8121 18.8412 12.6551 18.4661C12.4981 18.0909 12.675 17.6595 13.0501 17.5025C15.0952 16.6467 16.7134 14.9692 17.4919 12.8831C17.6341 12.5021 18.0583 12.3085 18.4393 12.4507Z" fill="white" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M7.56245 1.56016C7.70495 1.94106 7.51169 2.36536 7.1308 2.50786C5.03326 3.29259 3.34878 4.92965 2.49933 6.9978C2.34482 7.37399 1.9146 7.55369 1.53842 7.39918C1.16223 7.24467 0.982523 6.81445 1.13703 6.43827C2.14164 3.99234 4.13199 2.05736 6.61475 1.1285C6.99565 0.986002 7.41995 1.17926 7.56245 1.56016Z" fill="white" />
|
||||
<path d="M1.81818 15.7273C1.36631 15.7273 1 15.361 1 14.9091C1 14.4572 1.36631 14.0909 1.81818 14.0909C2.27005 14.0909 2.63636 14.4572 2.63636 14.9091C2.63636 15.361 2.27005 15.7273 1.81818 15.7273Z" fill="white" />
|
||||
</svg>
|
||||
</UniControlButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
.uni-controls {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--, #ecedef);
|
||||
background: #fff;
|
||||
|
||||
box-shadow: 0px 2px 4px 0px rgba(30, 34, 43, 0.06);
|
||||
width: fit-content;
|
||||
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.uni-control-button {
|
||||
display: flex;
|
||||
padding: var(--radius-m, 6px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(30, 34, 43, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@slider-bg: rgb(var(--text-color-secondary));
|
||||
@slider-width: 116px;
|
||||
@slider-height: 2px;
|
||||
@slider-radius: 15px;
|
||||
|
||||
@slider-handle-size: 12px;
|
||||
|
||||
@slider-value-width: 55px;
|
||||
@slider-value-height: 28px;
|
||||
|
||||
@slider-parts-hover-color: rgb(var(--border-color));
|
||||
|
||||
:export {
|
||||
--slider-width: unit(@slider-width);
|
||||
--slider-handle-size: unit(@slider-handle-size);
|
||||
}
|
||||
|
||||
.slider {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
gap: var(--margin-xxs);
|
||||
align-items: center;
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
.slider {
|
||||
&-rail {
|
||||
background-color: rgb(var(--grey-300));
|
||||
}
|
||||
|
||||
&-handle {
|
||||
background-color: rgb(var(--grey-300));
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
background-color: rgb(var(--grey-300));
|
||||
}
|
||||
}
|
||||
|
||||
&-reset-point {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&-value {
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
background-color: rgb(var(--grey-300));
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
pointer-events: none;
|
||||
color: rgb(var(--grey-300));
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-rail {
|
||||
@padding: (@slider-handle-size / 2);
|
||||
|
||||
position: relative;
|
||||
|
||||
width: @slider-width;
|
||||
height: @slider-height;
|
||||
padding: 0 @padding;
|
||||
|
||||
background-color: @slider-bg;
|
||||
border-radius: @slider-radius;
|
||||
}
|
||||
|
||||
&-inner-rail {
|
||||
position: relative;
|
||||
height: @slider-height;
|
||||
}
|
||||
|
||||
&-handle {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
top: calc(50% - @slider-handle-size / 2);
|
||||
transform: translateX(-50%);
|
||||
|
||||
width: @slider-handle-size;
|
||||
height: @slider-handle-size;
|
||||
|
||||
background-color: rgb(var(--color-white));
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px 0 rgb(0 0 0 / 10%);
|
||||
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: @slider-parts-hover-color;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-reset-point {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
box-sizing: border-box;
|
||||
width: @slider-height * 2;
|
||||
height: @slider-height;
|
||||
|
||||
background-color: rgb(var(--color-white));
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&-value {
|
||||
cursor: pointer;
|
||||
|
||||
width: @slider-value-width;
|
||||
height: @slider-value-height;
|
||||
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 2;
|
||||
color: rgb(var(--text-color-secondary-darker));
|
||||
text-align: center;
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: @slider-parts-hover-color;
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.dropdown-open {
|
||||
background-color: @slider-parts-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-shortcuts {
|
||||
transform: translateX(-(@slider-value-width / 2));
|
||||
|
||||
display: grid;
|
||||
gap: var(--margin-xxs);
|
||||
align-items: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 128px;
|
||||
padding: var(--padding-sm);
|
||||
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
background-color: rgb(var(--color-white));
|
||||
border: 1px solid rgb(var(--border-color));
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
&-shortcut {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--padding-xs) 0 var(--padding-xs) 36px;
|
||||
|
||||
color: rgb(var(--text-color));
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
transition: background 0.2s;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--bg-color-hover));
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&-shortcut-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--padding-xs);
|
||||
height: 100%;
|
||||
color: rgb(var(--success-color));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-shortcut-active {
|
||||
background-color: rgb(var(--bg-color-hover));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { CloseSingle } from '@univerjs/icons';
|
||||
import { useDependency } from '@univerjs/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import type { ISidebarMethodOptions } from '@univerjs/ui';
|
||||
import { CustomLabel, ILeftSidebarService, ISidebarService, useObservable } from '@univerjs/ui';
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface IUniSidebarProps {
|
||||
position: 'left' | 'right';
|
||||
sidebarService: ISidebarService;
|
||||
showClose?: boolean;
|
||||
}
|
||||
|
||||
export function LeftSidebar() {
|
||||
const sidebarService = useDependency(ILeftSidebarService);
|
||||
return <UniSidebar position="left" sidebarService={sidebarService} showClose={false} />;
|
||||
}
|
||||
|
||||
export function RightSidebar() {
|
||||
const sidebarService = useDependency(ISidebarService);
|
||||
return <UniSidebar position="right" sidebarService={sidebarService} />;
|
||||
}
|
||||
|
||||
export function UniSidebar(props: IUniSidebarProps) {
|
||||
const { sidebarService, position, showClose = true } = props;
|
||||
const sidebarOptions = useObservable<ISidebarMethodOptions>(sidebarService.sidebarOptions$);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!sidebarOptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const copy = { ...sidebarOptions } as Omit<ISidebarMethodOptions, 'children'> & {
|
||||
children?: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
};
|
||||
|
||||
for (const key of ['children', 'header', 'footer']) {
|
||||
const k = key as keyof ISidebarMethodOptions;
|
||||
|
||||
if (sidebarOptions[k]) {
|
||||
const props = sidebarOptions[k] as any;
|
||||
|
||||
if (props) {
|
||||
(copy as any)[k] = <CustomLabel {...props} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
}, [sidebarOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = (e: Event) => {
|
||||
sidebarService.scrollEvent$.next(e);
|
||||
};
|
||||
const scrollElement = scrollRef.current;
|
||||
if (scrollElement) {
|
||||
scrollElement.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
|
||||
return () => {
|
||||
scrollElement?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [sidebarService]);
|
||||
|
||||
const rootClassName = clsx(styles.uniSidebar, {
|
||||
[styles.uniSidebarOpen]: options?.visible,
|
||||
[styles.uniSidebarLeft]: position === 'left',
|
||||
[styles.uniSidebarRight]: position === 'right',
|
||||
});
|
||||
|
||||
const width = useMemo(() => {
|
||||
if (!options?.visible) return 0;
|
||||
|
||||
if (typeof options.width === 'number') {
|
||||
return `${options.width}px`;
|
||||
}
|
||||
|
||||
return options.width;
|
||||
}, [options]);
|
||||
|
||||
function handleClose() {
|
||||
const options = {
|
||||
...sidebarOptions,
|
||||
visible: false,
|
||||
};
|
||||
|
||||
sidebarService.sidebarOptions$.next(options);
|
||||
options?.onClose?.();
|
||||
}
|
||||
return (
|
||||
<aside className={rootClassName} style={{ width }}>
|
||||
<section className={styles.uniSidebarContainer} ref={scrollRef}>
|
||||
{ showClose && (
|
||||
<header className={styles.uniSidebarHeader}>
|
||||
{options?.header}
|
||||
|
||||
<a className={styles.uniSidebarHeaderClose} onClick={handleClose}>
|
||||
<CloseSingle />
|
||||
</a>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<section className={styles.uniSidebarBody}>{options?.children}</section>
|
||||
|
||||
{options?.footer && <footer className={styles.uniSidebarFooter}>{options.footer}</footer>}
|
||||
</section>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.uni-sidebar {
|
||||
position: fixed;
|
||||
|
||||
border: 1px solid rgb(var(--border-color));
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
top: 72px;
|
||||
bottom: 12px;
|
||||
z-index: 20;
|
||||
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
color: rgb(var(--text-color));
|
||||
|
||||
transition: all 0.15s;
|
||||
|
||||
&-left {
|
||||
left: 12px;
|
||||
transform: translateX(calc(-100% - 12px));
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&-right {
|
||||
min-width: 280px;
|
||||
max-width: 400px;
|
||||
right: 12px;
|
||||
transform: translateX(calc(100% + 12px));
|
||||
}
|
||||
|
||||
&&-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&-container {
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
// grid-template-rows: auto 1fr auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
min-height: 100%;
|
||||
margin: auto;
|
||||
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
|
||||
scrollbar-color: rgba(var(--scrollbar-color), 0.7) transparent;
|
||||
scrollbar-gutter: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 44px;
|
||||
padding: var(--padding-xl) var(--padding-xl) 0;
|
||||
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 500;
|
||||
|
||||
background: rgb(var(--bg-color-secondary));
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
|
||||
&-close {
|
||||
cursor: pointer;
|
||||
color: rgb(var(--text-color-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: var(--padding-sm);
|
||||
box-sizing: border-box;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: var(--padding-xl);
|
||||
background: rgb(var(--bg-color-secondary));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { flip, offset, shift, useFloating } from '@floating-ui/react-dom';
|
||||
import React, { useEffect, useImperativeHandle } from 'react';
|
||||
import { IUniverInstanceService, type Nullable, useDependency, useObservable } from '@univerjs/core';
|
||||
import { ComponentContainer, ToolbarItem, useComponentsOfPart, useToolbarGroups } from '@univerjs/ui';
|
||||
import type { IUnitRendererProps } from '../workbench/UniWorkbench';
|
||||
import { DELETE_MENU_ID, DOWNLOAD_MENU_ID, LOCK_MENU_ID, PRINT_MENU_ID, SHARE_MENU_ID, UNI_MENU_POSITIONS, ZEN_MENU_ID } from '../../controllers/menu';
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface IFloatingToolbarRef {
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
export enum UniFloatToolbarUIPart {
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
const MENU_POSITIONS = [
|
||||
UNI_MENU_POSITIONS.TOOLBAR_FLOAT,
|
||||
];
|
||||
|
||||
const UNI_FLOATING_TOOLBAR_SCHEMA: string[] = [
|
||||
DOWNLOAD_MENU_ID,
|
||||
SHARE_MENU_ID,
|
||||
LOCK_MENU_ID,
|
||||
PRINT_MENU_ID,
|
||||
ZEN_MENU_ID,
|
||||
DELETE_MENU_ID,
|
||||
];
|
||||
|
||||
export const UniFloatingToolbar = React.forwardRef<IFloatingToolbarRef, { node: Nullable<IUnitRendererProps> }>(({ node }, ref) => {
|
||||
const { x, y, refs, strategy, update } = useFloating({
|
||||
placement: 'top',
|
||||
middleware: [offset(10), flip(), shift({ padding: 5 })],
|
||||
});
|
||||
|
||||
const { visibleItems } = useToolbarGroups(MENU_POSITIONS, UNI_MENU_POSITIONS.TOOLBAR_FLOAT);
|
||||
const uniVisibleItems = UNI_FLOATING_TOOLBAR_SCHEMA.map((id) => visibleItems.find((item) => item.id === id)).filter((item) => !!item);
|
||||
|
||||
const { setReference, setFloating } = refs;
|
||||
|
||||
const toolbarNameComponents = useComponentsOfPart(UniFloatToolbarUIPart.NAME);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
update: () => update(),
|
||||
}), [update]);
|
||||
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
const ref = document.querySelector(`[data-id="${node.unitId}"]`);
|
||||
setReference(ref);
|
||||
}
|
||||
}, [node, setReference]);
|
||||
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.uniFloatingToolbar}
|
||||
ref={setFloating}
|
||||
style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
|
||||
>
|
||||
<div className={styles.uniToolbar}>
|
||||
<ComponentContainer
|
||||
key="name"
|
||||
components={toolbarNameComponents}
|
||||
fallback={<UnitName unitId={node.unitId} />}
|
||||
/>
|
||||
<UniDiv />
|
||||
<div className={styles.toolbarGroup}>
|
||||
{uniVisibleItems.map((subItem) => <ToolbarItem key={subItem.id} {...subItem} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export function UniDiv() {
|
||||
return <div className={styles.uniDiv}></div>;
|
||||
}
|
||||
|
||||
export function UnitName({ unitId }: { unitId: string }) {
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
const unit = instanceService.getUnit(unitId);
|
||||
const name = useObservable(unit?.name$);
|
||||
return (
|
||||
<div className={styles.unitName}>
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 React, { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { CheckMarkSingle, CloseSingle, FxSingle } from '@univerjs/icons';
|
||||
import type { Nullable, Workbook } from '@univerjs/core';
|
||||
import { BooleanNumber, DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DocumentFlavor, HorizontalAlign, IPermissionService, IUniverInstanceService, Rectangle, ThemeService, UniverInstanceType, useDependency, useObservable, VerticalAlign, WrapStrategy } from '@univerjs/core';
|
||||
import { IEditorBridgeService, IFormulaEditorManagerService, useActiveWorkbook } from '@univerjs/sheets-ui';
|
||||
import { RangeProtectionPermissionEditPoint, RangeProtectionRuleModel, SheetsSelectionsService, WorkbookEditablePermission, WorksheetEditPermission, WorksheetProtectionRuleModel, WorksheetSetCellValuePermission } from '@univerjs/sheets';
|
||||
import { EMPTY, merge, switchMap } from 'rxjs';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import { KeyCode, ProgressBar, TextEditor } from '@univerjs/ui';
|
||||
import clsx from 'clsx';
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const UniFormulaBar = () => {
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
const visibleInfo = useObservable(editorBridgeService.visible$);
|
||||
const focusedId = useObservable(univerInstanceService.focused$);
|
||||
const visible = (visibleInfo?.visible && focusedId) || false;
|
||||
|
||||
const handleOpenWrite = useCallback(() => {
|
||||
if (focusedId) {
|
||||
editorBridgeService.changeVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.PointerDown,
|
||||
unitId: focusedId,
|
||||
});
|
||||
}
|
||||
}, [editorBridgeService, focusedId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(styles.uniFormulaBar, {
|
||||
[styles.uniFormulaBarDisable]: !focusedId,
|
||||
})}
|
||||
onClick={() => handleOpenWrite()}
|
||||
>
|
||||
<FxSingle />
|
||||
<span className={styles.uniFormulaBarText}>
|
||||
Write formula
|
||||
</span>
|
||||
</div>
|
||||
{visible && (
|
||||
<div
|
||||
className={styles.uniFormulaBarFullInput}
|
||||
>
|
||||
<FormulaBar />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function FormulaBar() {
|
||||
const formulaEditorManagerService = useDependency(IFormulaEditorManagerService);
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const themeService = useDependency(ThemeService);
|
||||
const progressBarColor = themeService.getCurrentTheme().primaryColor;
|
||||
const [disable, setDisable] = useState<boolean>(false);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const selectionManager = useDependency(SheetsSelectionsService);
|
||||
const worksheetProtectionRuleModel = useDependency(WorksheetProtectionRuleModel);
|
||||
const rangeProtectionRuleModel = useDependency(RangeProtectionRuleModel);
|
||||
const permissionService = useDependency(IPermissionService);
|
||||
const currentWorkbook = useActiveWorkbook();
|
||||
|
||||
function getPermissionIds(unitId: string, subUnitId: string): string[] {
|
||||
return [
|
||||
new WorkbookEditablePermission(unitId).id,
|
||||
new WorksheetSetCellValuePermission(unitId, subUnitId).id,
|
||||
new WorksheetEditPermission(unitId, subUnitId).id,
|
||||
];
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const subscription = merge(
|
||||
worksheetProtectionRuleModel.ruleChange$,
|
||||
rangeProtectionRuleModel.ruleChange$,
|
||||
selectionManager.selectionMoveEnd$,
|
||||
workbook.activeSheet$
|
||||
).pipe(
|
||||
switchMap(() => {
|
||||
const unitId = workbook.getUnitId();
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return EMPTY;
|
||||
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
const range = selectionManager.getCurrentLastSelection()?.range;
|
||||
if (!range) return EMPTY;
|
||||
|
||||
const permissionIds = getPermissionIds(unitId, subUnitId);
|
||||
|
||||
const selectionRanges = selectionManager.getCurrentSelections()?.map((selection) => selection.range);
|
||||
const permissionList = rangeProtectionRuleModel.getSubunitRuleList(unitId, subUnitId).filter((rule) => {
|
||||
return rule.ranges.some((r) => selectionRanges?.some((selectionRange) => Rectangle.intersects(r, selectionRange)));
|
||||
});
|
||||
|
||||
permissionList.forEach((p) => {
|
||||
permissionIds.push(new RangeProtectionPermissionEditPoint(unitId, subUnitId, p.permissionId).id);
|
||||
});
|
||||
|
||||
return permissionService.composePermission$(permissionIds);
|
||||
})
|
||||
).subscribe((permissions) => {
|
||||
if (permissions) {
|
||||
setDisable(!permissions.every((p) => p.value));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const INITIAL_SNAPSHOT = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: `${DEFAULT_EMPTY_DOCUMENT_VALUE}`,
|
||||
textRuns: [],
|
||||
paragraphs: [
|
||||
{
|
||||
startIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: Number.POSITIVE_INFINITY,
|
||||
height: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
documentFlavor: DocumentFlavor.MODERN,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
marginRight: 0,
|
||||
marginLeft: 0,
|
||||
paragraphLineGapDefault: 0,
|
||||
renderConfig: {
|
||||
horizontalAlign: HorizontalAlign.UNSPECIFIED,
|
||||
verticalAlign: VerticalAlign.TOP,
|
||||
centerAngle: 0,
|
||||
vertexAngle: 0,
|
||||
wrapStrategy: WrapStrategy.WRAP,
|
||||
isRenderStyle: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function resizeCallBack(editor: Nullable<HTMLDivElement>) {
|
||||
if (editor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editorRect = editor.getBoundingClientRect();
|
||||
|
||||
formulaEditorManagerService.setPosition(editorRect);
|
||||
}
|
||||
|
||||
// Handle click the "×" button.
|
||||
function handleCloseBtnClick() {
|
||||
const visibleState = editorBridgeService.isVisible();
|
||||
if (visibleState.visible) {
|
||||
editorBridgeService.changeVisible({
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode: KeyCode.ESC,
|
||||
unitId: currentWorkbook?.getUnitId() ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle click the "√" button.
|
||||
function handleConfirmBtnClick() {
|
||||
const visibleState = editorBridgeService.isVisible();
|
||||
if (visibleState.visible) {
|
||||
editorBridgeService.changeVisible({
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.PointerDown,
|
||||
unitId: currentWorkbook?.getUnitId() ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handlerFxBtnClick() {
|
||||
formulaEditorManagerService.handleFxBtnClick(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.uniFormulaBox}>
|
||||
<TextEditor
|
||||
id={DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY}
|
||||
isSheetEditor={true}
|
||||
resizeCallBack={resizeCallBack}
|
||||
cancelDefaultResizeListener={true}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={clsx(styles.uniFormulaInput, styles.formulaContent)}
|
||||
snapshot={INITIAL_SNAPSHOT}
|
||||
isSingle={true}
|
||||
/>
|
||||
<div className={clsx(styles.formulaIcon, { [styles.formulaIconDisable]: disable })}>
|
||||
<span
|
||||
className={clsx(styles.iconContainer, styles.iconContainerError)}
|
||||
onClick={handleCloseBtnClick}
|
||||
>
|
||||
<CloseSingle />
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={clsx(styles.iconContainer, styles.iconContainerSuccess)}
|
||||
onClick={handleConfirmBtnClick}
|
||||
>
|
||||
<CheckMarkSingle />
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar barColor={progressBarColor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Inc.
|
||||
*
|
||||
* 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 { IUniverInstanceService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core';
|
||||
import { MenuPosition, ToolbarItem, useToolbarGroups } from '@univerjs/ui';
|
||||
import type { ComponentType } from 'react';
|
||||
import React from 'react';
|
||||
import { useWorkbooks } from '@univerjs/sheets-ui';
|
||||
import { UNI_MENU_POSITIONS } from '../../controllers/menu';
|
||||
import { UniToolbarService } from '../../services/toolbar/uni-toolbar-service';
|
||||
import styles from './index.module.less';
|
||||
import { UniFormulaBar } from './UniFormulaBar';
|
||||
|
||||
const MENU_POSITIONS = [
|
||||
MenuPosition.TOOLBAR_START,
|
||||
MenuPosition.TOOLBAR_INSERT,
|
||||
MenuPosition.TOOLBAR_FORMULAS,
|
||||
MenuPosition.TOOLBAR_DATA,
|
||||
MenuPosition.TOOLBAR_VIEW,
|
||||
UNI_MENU_POSITIONS.TOOLBAR_MAIN,
|
||||
];
|
||||
|
||||
export interface IToolbarProps {
|
||||
headerMenuComponents?: Set<ComponentType>;
|
||||
}
|
||||
|
||||
export function UniToolbar() {
|
||||
const uniToolbarService = useDependency(UniToolbarService);
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
const focusedUnit = useObservable(instanceService.focused$);
|
||||
|
||||
const type = focusedUnit ? (instanceService.getUnit(focusedUnit)?.type ?? UniverInstanceType.UNIVER_UNKNOWN) : UniverInstanceType.UNIVER_UNKNOWN;
|
||||
|
||||
const { visibleItems } = useToolbarGroups(MENU_POSITIONS);
|
||||
|
||||
const toolbarItems = uniToolbarService.getItems();
|
||||
|
||||
const uniVisibleItems = toolbarItems.map((item) => {
|
||||
const { impl } = item;
|
||||
const id = impl.find((item) => item.type === type) || impl.find((item) => item.type === UniverInstanceType.UNIVER_UNKNOWN);
|
||||
if (id !== undefined) {
|
||||
return visibleItems.find((item) => item.id === id.id);
|
||||
}
|
||||
return null;
|
||||
}).filter((item) => !!item);
|
||||
|
||||
const hasWorkbooks = useWorkbooks().length > 0;
|
||||
|
||||
return (
|
||||
<div className={styles.uniToolbar}>
|
||||
{ hasWorkbooks && <UniFormulaBar /> }
|
||||
<div className={styles.toolbarGroup}>
|
||||
{uniVisibleItems.map((subItem) => <ToolbarItem key={subItem.id} {...subItem} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
@toolbar-btn-size: 24px;
|
||||
|
||||
.headerbar {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
> .header-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
|
||||
> div {
|
||||
user-select: none;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
height: 28px;
|
||||
margin-left: 4px;
|
||||
padding: 2px 4px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-300));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menubar {
|
||||
display: flex;
|
||||
gap: var(--margin-xs);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 32px;
|
||||
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
|
||||
display: block;
|
||||
|
||||
padding: var(--padding-xs) var(--padding-sm);
|
||||
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 400;
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
transition: all 0.1s ease-in-out;
|
||||
|
||||
&:hover:not(&-active) {
|
||||
background: rgb(var(--grey-300));
|
||||
}
|
||||
|
||||
&-active {
|
||||
color: rgb(var(--color-white));
|
||||
background: rgb(var(--primary-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uni-toolbar {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--, #ecedef);
|
||||
overflow: hidden;
|
||||
|
||||
box-shadow: 0px 2px 4px 0px rgba(30, 34, 43, 0.06);
|
||||
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
font-size: var(--font-size-base);
|
||||
color: rgb(var(--text-color));
|
||||
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
|
||||
&-container {
|
||||
padding: 0 var(--padding-lg);
|
||||
display: flex;
|
||||
gap: var(--margin-xs);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-more {
|
||||
max-width: 80%;
|
||||
|
||||
&-container {
|
||||
display: grid;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--padding-sm);
|
||||
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: var(--box-shadow-base);
|
||||
|
||||
.toolbar-group {
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
content: initial;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-bottom: var(--margin-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
gap: var(--margin-xs);
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child, :empty) {
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
display: block;
|
||||
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
|
||||
background-color: rgb(var(--border-color));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
&-select-button {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
gap: var(--margin-xs);
|
||||
align-items: center;
|
||||
|
||||
height: @toolbar-btn-size;
|
||||
padding-right: 18px;
|
||||
|
||||
font-size: var(--font-size-lg);
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
color: rgb(var(--grey-200));
|
||||
&.toolbar-item-select-button-activated {
|
||||
color: rgba(var(--text-color), 0.25);
|
||||
.toolbar-item-select-button {
|
||||
&-label,
|
||||
&-arrow {
|
||||
&:hover {
|
||||
background-color: rgb(
|
||||
var(--grey-100)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-activated {
|
||||
background-color: rgb(var(--grey-100));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-100));
|
||||
|
||||
.toolbar {
|
||||
&-item {
|
||||
&-select-button {
|
||||
&-label {
|
||||
background-color: rgb(var(--grey-100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
padding: 0 var(--padding-xs);
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-300)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-right: var(--padding-xs);
|
||||
|
||||
color: rgb(var(--grey-400));
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-300));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-select {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
gap: var(--margin-xs);
|
||||
align-items: center;
|
||||
|
||||
height: @toolbar-btn-size;
|
||||
padding: 0 var(--padding-xs);
|
||||
|
||||
font-size: var(--font-size-lg);
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
color: rgb(var(--grey-200));
|
||||
&.toolbar-item-select-activated {
|
||||
color: rgba(var(--text-color), 0.25);
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-activated {
|
||||
background-color: rgb(var(--grey-100));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-100));
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
|
||||
color: rgb(var(--grey-400));
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
color: rgb(var(--grey-200));
|
||||
&.toolbar-item-select-arrow-activated {
|
||||
color: rgba(var(--text-color), 0.25);
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-activated {
|
||||
background-color: rgb(var(--grey-100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uni-formula-bar {
|
||||
display: flex;
|
||||
width: 133px;
|
||||
padding: var(--radius-m, 6px) 12px;
|
||||
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: var(--radius-m, 6px);
|
||||
|
||||
background: #f0f3fe;
|
||||
color: var(---Hyacinth-Blue-500, #274fee);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 153.846% */
|
||||
|
||||
&-text {
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
&-disable {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.uni-floating-toolbar {
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.unit-name {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
padding: 0px 4px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: var(--radius-m, 6px);
|
||||
background: var(---White, #fff);
|
||||
color: #1e222b;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.uni-div {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: #ecedef;
|
||||
}
|
||||
|
||||
.uni-formula-bar-full-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(---White, #fff);
|
||||
z-index: 10;
|
||||
|
||||
.uni-formula-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.uni-formula-input {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
background: #f0f3fe;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-m, 6px);
|
||||
border: 1px solid var(---Hyacinth-Blue-500, #274fee);
|
||||
background: #f0f3fe;
|
||||
box-shadow: 0px 0px 4px 0px rgba(39, 79, 238, 0.4);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.formula-icon {
|
||||
padding: 6px 0;
|
||||
|
||||
&-disable {
|
||||
color: rgb(var(--grey-200));
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
border-right: 1px solid rgb(var(--border-color));
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: var(--padding-xs);
|
||||
|
||||
font-size: var(--font-size-lg);
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
&-success {
|
||||
&.formula-active {
|
||||
cursor: pointer;
|
||||
color: rgb(var(--success-color));
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-error {
|
||||
&.formula-active {
|
||||
cursor: pointer;
|
||||
color: rgb(var(--error-color));
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-fx {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgb(var(--grey-50));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.formula-icon {
|
||||
display: flex;
|
||||
}
|
||||
.icon-container {
|
||||
display: flex;
|
||||
padding: var(--radius-m, 6px);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background: var(--hyacinth-blue-5007, rgba(39, 79, 238, 0.07));
|
||||
color: #274fee;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
// Refer to packages/ui/src/views/App.tsx
|
||||
|
||||
import { IContextService, IUniverInstanceService, LocaleService, ThemeService, useDependency } from '@univerjs/core';
|
||||
import { ConfigContext, ConfigProvider, defaultTheme, themeInstance } from '@univerjs/design';
|
||||
import { debounce, ICommandService, IContextService, IUniverInstanceService, LocaleService, ThemeService, useDependency } from '@univerjs/core';
|
||||
import type { ILocale } from '@univerjs/design';
|
||||
import { ConfigContext, ConfigProvider, defaultTheme, themeInstance } from '@univerjs/design';
|
||||
import {
|
||||
builtInGlobalComponents,
|
||||
BuiltInUIPart,
|
||||
@@ -26,31 +26,49 @@ import {
|
||||
ContextMenu,
|
||||
IMessageService,
|
||||
type IWorkbenchOptions,
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
UNI_DISABLE_CHANGING_FOCUS_KEY,
|
||||
useComponentsOfPart,
|
||||
useObservable,
|
||||
ZenZone,
|
||||
} from '@univerjs/ui';
|
||||
import type {
|
||||
NodeTypes,
|
||||
ReactFlowInstance,
|
||||
Viewport,
|
||||
} from '@xyflow/react';
|
||||
import {
|
||||
Background,
|
||||
NodeResizer,
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import clsx from 'clsx';
|
||||
import { UnitGridService } from '../../services/unit-grid/unit-grid.service';
|
||||
import styles from './workbench.module.less';
|
||||
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { MenuSingle } from '@univerjs/icons';
|
||||
import { UniFocusUnitOperation } from '../../commands/operations/uni-focus-unit.operation';
|
||||
import { FlowManagerService } from '../../services/flow/flow-manager.service';
|
||||
import { IUnitGridService } from '../../services/unit-grid/unit-grid.service';
|
||||
import { useUnitFocused, useUnitTitle } from '../hooks/title';
|
||||
import { DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM, UniControlItem, UniControls } from '../uni-controls/UniControls';
|
||||
import { LeftSidebar, RightSidebar } from '../uni-sidebar/UniSidebar';
|
||||
import { type IFloatingToolbarRef, UniFloatingToolbar } from '../uni-toolbar/UniFloatToolbar';
|
||||
import { UniToolbar } from '../uni-toolbar/UniToolbar';
|
||||
import styles from './workbench.module.less';
|
||||
// Refer to packages/ui/src/views/workbench/Workbench.tsx
|
||||
|
||||
export interface IUniWorkbenchProps extends IWorkbenchOptions {
|
||||
mountContainer: HTMLElement;
|
||||
|
||||
onRendered: () => void;
|
||||
onRendered: (contentEl: HTMLElement) => void;
|
||||
}
|
||||
|
||||
export function UniWorkbench(props: IUniWorkbenchProps) {
|
||||
const {
|
||||
header = true,
|
||||
footer = true,
|
||||
contextMenu = true,
|
||||
mountContainer,
|
||||
onRendered,
|
||||
@@ -59,36 +77,50 @@ export function UniWorkbench(props: IUniWorkbenchProps) {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const themeService = useDependency(ThemeService);
|
||||
const messageService = useDependency(IMessageService);
|
||||
const unitGridService = useDependency(UnitGridService);
|
||||
const unitGridService = useDependency(IUnitGridService);
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const flowManagerService = useDependency(FlowManagerService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const floatingToolbarRef = useRef<IFloatingToolbarRef>(null);
|
||||
const reactFlowInstance = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const footerComponents = useComponentsOfPart(BuiltInUIPart.FOOTER);
|
||||
const headerComponents = useComponentsOfPart(BuiltInUIPart.HEADER);
|
||||
const headerMenuComponents = useComponentsOfPart(BuiltInUIPart.HEADER_MENU);
|
||||
const contentComponents = useComponentsOfPart(BuiltInUIPart.CONTENT);
|
||||
const leftSidebarComponents = useComponentsOfPart(BuiltInUIPart.LEFT_SIDEBAR);
|
||||
const globalComponents = useComponentsOfPart(BuiltInUIPart.GLOBAL);
|
||||
|
||||
const unitGrid = useObservable(unitGridService.unitGrid$, undefined, true);
|
||||
const focused = useObservable(instanceService.focused$);
|
||||
|
||||
const focusUnit = useCallback((unitId: string) => {
|
||||
instanceService.focusUnit(unitId);
|
||||
instanceService.setCurrentUnitForType(unitId);
|
||||
}, [instanceService]);
|
||||
const focusedUnit = useObservable(instanceService.focused$);
|
||||
|
||||
useEffect(() => {
|
||||
if (!themeService.getCurrentTheme()) {
|
||||
themeService.setTheme(defaultTheme);
|
||||
}
|
||||
|
||||
onRendered();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
onRendered?.(contentRef.current);
|
||||
}
|
||||
}, [onRendered]);
|
||||
|
||||
const [locale, setLocale] = useState<ILocale>(localeService.getLocales() as unknown as ILocale);
|
||||
const [zoom, setZoom] = useState<number>(DEFAULT_ZOOM);
|
||||
|
||||
const onControlItemClick = useCallback((key: UniControlItem) => {
|
||||
switch (key) {
|
||||
case UniControlItem.AI: {
|
||||
commandService.executeCommand('project.controls-pro-search.operation');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [commandService]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const resizeUnits = useCallback(debounce(() => {
|
||||
renderManagerService.getRenderAll().forEach(((renderer) => renderer.engine.resize()));
|
||||
}, 400), [renderManagerService]); // TODO: this is not
|
||||
|
||||
// Create a portal container for injecting global component themes.
|
||||
const portalContainer = useMemo<HTMLElement>(() => document.createElement('div'), []);
|
||||
@@ -116,6 +148,43 @@ export function UniWorkbench(props: IUniWorkbenchProps) {
|
||||
};
|
||||
}, [localeService, messageService, mountContainer, portalContainer, themeService.currentTheme$]);
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
customNode: UnitNode,
|
||||
};
|
||||
|
||||
const unitGrid = useObservable(unitGridService.unitGrid$, undefined, true);
|
||||
const gridNodes = useMemo(() => unitGrid.map((item) => ({
|
||||
id: item.id,
|
||||
type: 'customNode',
|
||||
dragHandle: '.univer-uni-node-drag-handle',
|
||||
style: item.style,
|
||||
focusable: true,
|
||||
data: {
|
||||
unitId: item.data.unitId,
|
||||
gridService: unitGridService,
|
||||
instanceService,
|
||||
},
|
||||
position: item.position,
|
||||
})), [unitGrid]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(gridNodes);
|
||||
useEffect(() => {
|
||||
setNodes(gridNodes);
|
||||
}, [gridNodes]);
|
||||
|
||||
const onMove = useCallback((event: MouseEvent | TouchEvent | null, viewport: Viewport) => {
|
||||
floatingToolbarRef.current?.update();
|
||||
const { zoom } = viewport;
|
||||
setZoom(zoom);
|
||||
flowManagerService.setViewportChanged(viewport);
|
||||
}, [floatingToolbarRef, setZoom, flowManagerService]);
|
||||
|
||||
const onFlowInit = useCallback((instance: ReactFlowInstance<any>) => {
|
||||
flowManagerService.setReactFlowInstance(instance);
|
||||
}, [flowManagerService]);
|
||||
|
||||
const disableReactFlowBehavior = !!focusedUnit;
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={locale?.design} mountContainer={portalContainer}>
|
||||
{/**
|
||||
@@ -123,85 +192,102 @@ export function UniWorkbench(props: IUniWorkbenchProps) {
|
||||
* all focusin event merged from its descendants. The DesktopLayoutService would listen to focusin events
|
||||
* bubbled to this element and refocus the input element.
|
||||
*/}
|
||||
<div className={styles.workbenchLayout} tabIndex={-1} onBlur={(e) => e.stopPropagation()}>
|
||||
{/* header */}
|
||||
{header && (
|
||||
<header className={styles.workbenchContainerHeader}>
|
||||
<Toolbar headerMenuComponents={headerMenuComponents} />
|
||||
</header>
|
||||
)}
|
||||
<ReactFlowProvider>
|
||||
<div className={styles.workbenchLayout} tabIndex={-1} onBlur={(e) => e.stopPropagation()}>
|
||||
|
||||
{/* content */}
|
||||
<section className={styles.workbenchContainer}>
|
||||
<div className={styles.workbenchContainerWrapper}>
|
||||
<aside className={styles.workbenchContainerLeftSidebar}>
|
||||
<ComponentContainer key="left-sidebar" components={leftSidebarComponents} />
|
||||
</aside>
|
||||
<div className={styles.flowLayer}>
|
||||
<section
|
||||
className={styles.workbenchContainerCanvasContainer}
|
||||
ref={contentRef}
|
||||
data-range-selector
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
>
|
||||
<ReactFlow
|
||||
ref={reactFlowInstance}
|
||||
maxZoom={MAX_ZOOM}
|
||||
minZoom={MIN_ZOOM}
|
||||
zoomOnDoubleClick={!disableReactFlowBehavior}
|
||||
zoomOnPinch={!disableReactFlowBehavior}
|
||||
zoomOnScroll={!disableReactFlowBehavior}
|
||||
panOnDrag={!disableReactFlowBehavior}
|
||||
panOnScroll={!disableReactFlowBehavior}
|
||||
nodes={nodes}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={(nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === 'dimensions' && node.resizing) {
|
||||
unitGridService.changeDimension(node.id, { width: node.dimensions!.width, height: node.dimensions!.height });
|
||||
} else if (node.type === 'position') {
|
||||
unitGridService.changePosition(node.id, { x: node.position!.x, y: node.position!.y });
|
||||
}
|
||||
});
|
||||
|
||||
<section className={styles.workbenchContainerContent}>
|
||||
<header>
|
||||
{header && <ComponentContainer key="header" components={headerComponents} />}
|
||||
</header>
|
||||
|
||||
<section
|
||||
className={styles.workbenchContainerCanvasContainer}
|
||||
ref={contentRef}
|
||||
data-range-selector
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onNodesChange(nodes);
|
||||
}}
|
||||
onResize={resizeUnits}
|
||||
fitView
|
||||
defaultViewport={{ zoom: MIN_ZOOM, x: 0, y: 0 }}
|
||||
onPointerDown={(event) => {
|
||||
if (event.target instanceof HTMLElement
|
||||
&& (
|
||||
event.target.classList.contains('univer-render-canvas')
|
||||
|| event.target.classList.contains('react-flow__resize-control'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
commandService.executeCommand(UniFocusUnitOperation.id, { unitId: null });
|
||||
}}
|
||||
onMove={onMove}
|
||||
onInit={onFlowInit}
|
||||
>
|
||||
{/* Render units. */}
|
||||
{unitGrid?.map((unitId) => (
|
||||
<UnitRenderer
|
||||
key={unitId}
|
||||
unitId={unitId}
|
||||
focused={focused === unitId}
|
||||
onFocus={focusUnit}
|
||||
gridService={unitGridService}
|
||||
instanceService={instanceService}
|
||||
/>
|
||||
))}
|
||||
<ComponentContainer key="content" components={contentComponents} />
|
||||
</section>
|
||||
<Background bgColor="#f4f6f8" color="#d9d9d9"></Background>
|
||||
</ReactFlow>
|
||||
|
||||
{/* Sheet cell editors etc. Their size would not be affected the scale of ReactFlow. */}
|
||||
<ComponentContainer key="content" components={contentComponents} />
|
||||
</section>
|
||||
|
||||
<aside className={styles.workbenchContainerSidebar}>
|
||||
<Sidebar />
|
||||
</aside>
|
||||
</div>
|
||||
<div className={styles.floatLayer}>
|
||||
{/* header */}
|
||||
{header && (
|
||||
<div className={styles.workbenchContainerHeader}>
|
||||
<div className={styles.workbenchToolbarWrapper}>
|
||||
<UniToolbar />
|
||||
<ComponentContainer key="header" components={headerComponents} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<UniFloatingToolbar ref={floatingToolbarRef} node={nodes.find((n) => n.id === focusedUnit)?.data} />
|
||||
|
||||
{/* footer */}
|
||||
{footer && (
|
||||
<footer className={styles.workbenchFooter}>
|
||||
<ComponentContainer key="footer" components={footerComponents} />
|
||||
</footer>
|
||||
)}
|
||||
<LeftSidebar />
|
||||
<RightSidebar />
|
||||
|
||||
<ZenZone />
|
||||
</section>
|
||||
</div>
|
||||
<ComponentContainer key="global" components={globalComponents} />
|
||||
<ComponentContainer key="built-in-global" components={builtInGlobalComponents} />
|
||||
{contextMenu && <ContextMenu />}
|
||||
<FloatingContainer />
|
||||
{/* uni mode controller buttons */}
|
||||
<UniControls zoom={zoom} onItemClick={onControlItemClick} />
|
||||
</div>
|
||||
</div>
|
||||
<ComponentContainer key="global" components={globalComponents} />
|
||||
<ComponentContainer key="built-in-global" components={builtInGlobalComponents} />
|
||||
{contextMenu && <ContextMenu />}
|
||||
<FloatingContainer />
|
||||
</ReactFlowProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface IUnitRendererProps {
|
||||
unitId: string;
|
||||
focused: boolean;
|
||||
|
||||
gridService: UnitGridService;
|
||||
instanceService: IUniverInstanceService;
|
||||
|
||||
onFocus: (unitId: string) => void;
|
||||
interface IUnitNodeProps {
|
||||
data: IUnitRendererProps;
|
||||
}
|
||||
|
||||
function UnitRenderer(props: IUnitRendererProps) {
|
||||
const { unitId, gridService, focused, onFocus } = props;
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
function UnitNode({ data }: IUnitNodeProps) {
|
||||
const { unitId } = data;
|
||||
const title = useUnitTitle(unitId);
|
||||
const focused = useUnitFocused(unitId);
|
||||
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
const contextService = useDependency(IContextService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const disableChangingUnitFocusing = useObservable(
|
||||
() => contextService.subscribeContextValue$(UNI_DISABLE_CHANGING_FOCUS_KEY),
|
||||
false,
|
||||
@@ -211,9 +297,45 @@ function UnitRenderer(props: IUnitRendererProps) {
|
||||
|
||||
const focus = useCallback(() => {
|
||||
if (!disableChangingUnitFocusing && !focused) {
|
||||
onFocus(unitId);
|
||||
commandService.executeCommand(UniFocusUnitOperation.id, { unitId });
|
||||
}
|
||||
}, [focused, onFocus, disableChangingUnitFocusing, unitId]);
|
||||
}, [disableChangingUnitFocusing, focused, unitId, commandService]);
|
||||
|
||||
return (
|
||||
<div className={styles.uniNodeContainer} onPointerDownCapture={focus}>
|
||||
<NodeResizer isVisible={focused} minWidth={180} minHeight={100} />
|
||||
<UnitRenderer
|
||||
key={data.unitId}
|
||||
{...data}
|
||||
/>
|
||||
|
||||
<div className={styles.uniNodeDragHandle}>
|
||||
<MenuSingle />
|
||||
</div>
|
||||
|
||||
<div className={styles.uniNodeTitle}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export interface IUnitRendererProps {
|
||||
unitId: string;
|
||||
|
||||
gridService: IUnitGridService;
|
||||
instanceService: IUniverInstanceService;
|
||||
}
|
||||
|
||||
function UnitRenderer(props: IUnitRendererProps) {
|
||||
const { unitId, gridService } = props;
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const instanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
const focusedUnit = useObservable(instanceService.focused$);
|
||||
const focused = focusedUnit === unitId;
|
||||
|
||||
useEffect(() => {
|
||||
if (mountRef.current) {
|
||||
@@ -229,8 +351,7 @@ function UnitRenderer(props: IUnitRendererProps) {
|
||||
ref={mountRef}
|
||||
// We bind these focusing events on capture phrase so the
|
||||
// other event handlers would have correct currently focused unit.
|
||||
onPointerUpCapture={focus}
|
||||
onWheelCapture={focus}
|
||||
onWheel={(event) => event.stopPropagation()}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
// height: 32px;
|
||||
}
|
||||
|
||||
&-toolbar-wrapper {
|
||||
max-width: calc(100% - 700px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -23,6 +28,10 @@
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
@@ -31,6 +40,7 @@
|
||||
grid-template-rows: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&-content {
|
||||
@@ -48,12 +58,15 @@
|
||||
&-canvas-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-canvas {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&-focused {
|
||||
@@ -62,15 +75,77 @@
|
||||
}
|
||||
|
||||
&-left-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-sidebar {
|
||||
height: 100%;
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
// height: 36px;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
width: calc(100% - 700px);
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
border: 2px solid rgb(var(--border-color));
|
||||
border-radius: 8px;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.react-flow__attribution {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.float-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.uni-node {
|
||||
&-title {
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
left: 0;
|
||||
font-size: 11px;
|
||||
color: rgb(var(--grey-600));
|
||||
}
|
||||
|
||||
&-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-drag-handle {
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
padding: 4px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { UniverType } from '@univerjs/protocol';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { Disposable } from '../shared';
|
||||
|
||||
export { UniverType as UniverInstanceType } from '@univerjs/protocol';
|
||||
@@ -24,4 +25,7 @@ export type UnitType = UniverType | number;
|
||||
export abstract class UnitModel<_D = object, T extends UnitType = UnitType> extends Disposable {
|
||||
abstract readonly type: T;
|
||||
abstract getUnitId(): string;
|
||||
|
||||
abstract name$: Observable<string>;
|
||||
abstract setName(name: string): void;
|
||||
}
|
||||
|
||||
@@ -50,12 +50,21 @@ class DocumentDataModelSimple extends UnitModel<IDocumentData, UniverInstanceTyp
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected readonly _name$ = new BehaviorSubject<string>('');
|
||||
override name$ = this._name$.asObservable();
|
||||
|
||||
protected snapshot: IDocumentData;
|
||||
|
||||
constructor(snapshot: Partial<IDocumentData>) {
|
||||
super();
|
||||
|
||||
this.snapshot = { ...DEFAULT_DOC, ...snapshot };
|
||||
this._name$.next(this.snapshot.title ?? 'No Title');
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.snapshot.title = name;
|
||||
this._name$.next(name);
|
||||
}
|
||||
|
||||
get drawings() {
|
||||
@@ -206,9 +215,6 @@ export class DocumentDataModel extends DocumentDataModelSimple {
|
||||
|
||||
footerModelMap: Map<string, DocumentDataModel> = new Map();
|
||||
|
||||
private _name$ = new BehaviorSubject('');
|
||||
readonly name$ = this._name$.asObservable();
|
||||
|
||||
constructor(snapshot: Partial<IDocumentData>) {
|
||||
super(Tools.isEmptyObject(snapshot) ? getEmptySnapshot() : snapshot);
|
||||
|
||||
@@ -220,11 +226,6 @@ export class DocumentDataModel extends DocumentDataModelSimple {
|
||||
this._name$.next(this.snapshot.title ?? '');
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.snapshot.title = name;
|
||||
this._name$.next(name);
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this.headerModelMap.forEach((header) => {
|
||||
|
||||
@@ -409,8 +409,10 @@ export function insertCustomRanges(
|
||||
body.dataStream[oldCustomRange.endIndex] === DataStreamTreeTokenType.CUSTOM_RANGE_END;
|
||||
|
||||
if (isClosed) {
|
||||
insertCustomRanges.push(customRange);
|
||||
continue;
|
||||
}
|
||||
|
||||
// old is start
|
||||
if (body.dataStream[oldCustomRange.startIndex] === DataStreamTreeTokenType.CUSTOM_RANGE_START) {
|
||||
oldCustomRange.endIndex = customRange.endIndex;
|
||||
|
||||
@@ -50,3 +50,8 @@ export enum DataStreamTreeTokenType {
|
||||
LETTER = '',
|
||||
SPACE = ' ',
|
||||
}
|
||||
|
||||
/** Wrap your stream in a pair of custom range tokens. */
|
||||
export function makeCustomRangeStream(stream: string): string {
|
||||
return `${DataStreamTreeTokenType.CUSTOM_RANGE_START}${stream}${DataStreamTreeTokenType.CUSTOM_RANGE_END}`;
|
||||
}
|
||||
|
||||
@@ -153,6 +153,9 @@ export { SlideDataModel } from './slides/slide-model';
|
||||
export * from './types/const';
|
||||
export * from './types/enum';
|
||||
export * from './types/interfaces';
|
||||
|
||||
// #region deprecated - these modules shot not be here
|
||||
|
||||
export { ISnapshotServerService } from './services/snapshot/snapshot-server.service';
|
||||
export {
|
||||
transformSnapshotToWorkbookData,
|
||||
@@ -160,6 +163,7 @@ export {
|
||||
transformDocumentDataToSnapshot,
|
||||
transformSnapshotToDocumentData,
|
||||
generateTemporarySnap,
|
||||
generateTempDocumentSnapshot,
|
||||
} from './services/snapshot/snapshot-transform';
|
||||
export { textEncoder, textDecoder } from './services/snapshot/snapshot-utils';
|
||||
export { type ILogContext } from './services/log/context';
|
||||
@@ -167,6 +171,8 @@ export { b64DecodeUnicode, b64EncodeUnicode } from './shared/coder';
|
||||
export { ClientSnapshotServerService } from './services/snapshot/snapshot-server.service';
|
||||
export { getSheetBlocksFromSnapshot } from './services/snapshot/snapshot-transform';
|
||||
|
||||
// #endregion
|
||||
|
||||
export { isBlackColor, isWhiteColor } from './shared/color/color-kit';
|
||||
export { cellToRange } from './shared/common';
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export class CommandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
interface ICommandExecutionStackItem extends ICommandInfo {}
|
||||
interface ICommandExecutionStackItem extends ICommandInfo { }
|
||||
|
||||
export const NilCommand: ICommand = {
|
||||
id: 'nil',
|
||||
@@ -319,40 +319,45 @@ export class CommandService extends Disposable implements ICommandService {
|
||||
params?: P | undefined,
|
||||
options?: IExecutionOptions
|
||||
): R {
|
||||
const item = this._commandRegistry.getCommand(id);
|
||||
if (item) {
|
||||
const [command] = item;
|
||||
const commandInfo: ICommandInfo = {
|
||||
id: command.id,
|
||||
type: command.type,
|
||||
params,
|
||||
};
|
||||
try {
|
||||
const item = this._commandRegistry.getCommand(id);
|
||||
if (item) {
|
||||
const [command] = item;
|
||||
const commandInfo: ICommandInfo = {
|
||||
id: command.id,
|
||||
type: command.type,
|
||||
params,
|
||||
};
|
||||
|
||||
// If the executed command is of type `Mutation`, we should add a trigger params,
|
||||
// whose value is the command's ID that triggers the mutation.
|
||||
if (command.type === CommandType.MUTATION) {
|
||||
const triggerCommand = findLast(
|
||||
this._commandExecutionStack,
|
||||
(item) => item.type === CommandType.COMMAND
|
||||
);
|
||||
if (triggerCommand) {
|
||||
commandInfo.params = commandInfo.params ?? {};
|
||||
(commandInfo.params as IMutationCommonParams).trigger = triggerCommand.id;
|
||||
// If the executed command is of type `Mutation`, we should add a trigger params,
|
||||
// whose value is the command's ID that triggers the mutation.
|
||||
if (command.type === CommandType.MUTATION) {
|
||||
const triggerCommand = findLast(
|
||||
this._commandExecutionStack,
|
||||
(item) => item.type === CommandType.COMMAND
|
||||
);
|
||||
if (triggerCommand) {
|
||||
commandInfo.params = commandInfo.params ?? {};
|
||||
(commandInfo.params as IMutationCommonParams).trigger = triggerCommand.id;
|
||||
}
|
||||
}
|
||||
|
||||
const stackItemDisposable = this._pushCommandExecutionStack(commandInfo);
|
||||
|
||||
this._beforeCommandExecutionListeners.forEach((listener) => listener(commandInfo, options));
|
||||
const result = this._syncExecute<P, R>(command as ICommand<P, R>, params, options);
|
||||
this._commandExecutedListeners.forEach((listener) => listener(commandInfo, options));
|
||||
|
||||
stackItemDisposable.dispose();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const stackItemDisposable = this._pushCommandExecutionStack(commandInfo);
|
||||
|
||||
this._beforeCommandExecutionListeners.forEach((listener) => listener(commandInfo, options));
|
||||
const result = this._syncExecute<P, R>(command as ICommand<P, R>, params, options);
|
||||
this._commandExecutedListeners.forEach((listener) => listener(commandInfo, options));
|
||||
|
||||
stackItemDisposable.dispose();
|
||||
|
||||
return result;
|
||||
throw new Error(`[CommandService]: command "${id}" is not registered.`);
|
||||
} catch (error) {
|
||||
this._logService.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error(`[CommandService]: command "${id}" is not registered.`);
|
||||
}
|
||||
|
||||
private _pushCommandExecutionStack(stackItem: ICommandExecutionStackItem): IDisposable {
|
||||
|
||||
@@ -14,6 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// NOTE: editors has the following states:
|
||||
// 1. Not focused. The `contenteditable` is not focused and the editor would not respond
|
||||
// to keyboard events.
|
||||
// 2. Focused (but not activated). The `contenteditable` is focused and editor
|
||||
// would respond to keyboard events. This is the state when a user focus a spreadsheet
|
||||
// without the editor being visible.
|
||||
// 3. Activated. User can see the cursor blinking.
|
||||
|
||||
export const FOCUSING_UNIT = 'FOCUSING_UNIT';
|
||||
|
||||
export const FOCUSING_SHEET = 'FOCUSING_SHEET';
|
||||
export const FOCUSING_DOC = 'FOCUSING_DOC';
|
||||
export const FOCUSING_SLIDE = 'FOCUSING_SLIDE';
|
||||
@@ -25,14 +35,19 @@ export const EDITOR_ACTIVATED = 'EDITOR_ACTIVATED';
|
||||
export const FOCUSING_EDITOR_INPUT_FORMULA = 'FOCUSING_EDITOR_INPUT_FORMULA';
|
||||
|
||||
/** The focusing state of the formula editor (Fx bar). */
|
||||
export const FOCUSING_FORMULA_EDITOR = 'FOCUSING_FORMULA_EDITOR';
|
||||
export const FOCUSING_FX_BAR_EDITOR = 'FOCUSING_FX_BAR_EDITOR';
|
||||
|
||||
export const FOCUSING_UNIVER_EDITOR = 'FOCUSING_UNIVER_EDITOR';
|
||||
|
||||
export const FOCUSING_EDITOR_STANDALONE = 'FOCUSING_EDITOR_INPUT_FORMULA';
|
||||
|
||||
// WTF: what does this even means?
|
||||
export const FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE = 'FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE';
|
||||
|
||||
/**
|
||||
* The focusing state of the common drawings.
|
||||
*/
|
||||
export const FOCUSING_COMMON_DRAWINGS = 'FOCUSING_COMMON_DRAWINGS';
|
||||
|
||||
export const FORMULA_EDITOR_ACTIVATED = 'FORMULA_EDITOR_ACTIVATED';
|
||||
|
||||
|
||||
@@ -24,11 +24,22 @@ import type { Nullable } from '../../shared';
|
||||
import { Disposable } from '../../shared/lifecycle';
|
||||
import { Workbook } from '../../sheets/workbook';
|
||||
import { SlideDataModel } from '../../slides/slide-model';
|
||||
import { FOCUSING_DOC, FOCUSING_SHEET, FOCUSING_SLIDE } from '../context/context';
|
||||
import { FOCUSING_DOC, FOCUSING_SHEET, FOCUSING_SLIDE, FOCUSING_UNIT } from '../context/context';
|
||||
import { IContextService } from '../context/context.service';
|
||||
import type { UnitModel, UnitType } from '../../common/unit';
|
||||
import { UniverInstanceType } from '../../common/unit';
|
||||
|
||||
export type UnitCtor = new (...args: any[]) => UnitModel;
|
||||
|
||||
export interface ICreateUnitOptions {
|
||||
/**
|
||||
* If Univer should make the new unit as current of its type.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
makeCurrent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* IUniverInstanceService holds all the current univer instances and provides a set of
|
||||
* methods to add and remove univer instances.
|
||||
@@ -49,9 +60,10 @@ export interface IUniverInstanceService {
|
||||
/** Subscribe to curtain type of units' disposing. */
|
||||
getTypeOfUnitDisposed$<T extends UnitModel>(type: UnitType): Observable<T>;
|
||||
|
||||
/** An observable value that emits the id of the focused unit. */
|
||||
focused$: Observable<Nullable<string>>;
|
||||
/** Focus a unit. */
|
||||
focusUnit(unitId: string | null): void;
|
||||
|
||||
getFocusedUnit(): Nullable<UnitModel>;
|
||||
|
||||
getCurrentUnitForType<T extends UnitModel>(type: UnitType): Nullable<T>;
|
||||
@@ -60,7 +72,7 @@ export interface IUniverInstanceService {
|
||||
getCurrentTypeOfUnit$<T extends UnitModel>(type: UnitType): Observable<Nullable<T>>;
|
||||
|
||||
/** Create a unit with snapshot info. */
|
||||
createUnit<T, U extends UnitModel>(type: UnitType, data: Partial<T>): U;
|
||||
createUnit<T, U extends UnitModel>(type: UnitType, data: Partial<T>, options?: ICreateUnitOptions): U;
|
||||
/** Dispose a unit */
|
||||
disposeUnit(unitId: string): boolean;
|
||||
|
||||
@@ -98,13 +110,19 @@ export class UniverInstanceService extends Disposable implements IUniverInstance
|
||||
this._focused$.complete();
|
||||
}
|
||||
|
||||
private _createHandler!: (type: UnitType, data: unknown, ctor: new (...args: any[]) => UnitModel) => UnitModel;
|
||||
__setCreateHandler(handler: (type: UnitType, data: unknown, ctor: new (...args: any[]) => UnitModel) => UnitModel): void {
|
||||
private _createHandler!: (
|
||||
type: UnitType,
|
||||
data: unknown,
|
||||
ctor: UnitCtor,
|
||||
options?: ICreateUnitOptions,
|
||||
) => UnitModel;
|
||||
|
||||
__setCreateHandler(handler: (type: UnitType, data: unknown, ctor: UnitCtor, options?: ICreateUnitOptions) => UnitModel): void {
|
||||
this._createHandler = handler;
|
||||
}
|
||||
|
||||
createUnit<T, U extends UnitModel>(type: UnitType, data: T): U {
|
||||
const model = this._createHandler(type, data, this._ctorByType.get(type)!);
|
||||
createUnit<T, U extends UnitModel>(type: UnitType, data: T, options?: ICreateUnitOptions): U {
|
||||
const model = this._createHandler(type, data, this._ctorByType.get(type)!, options);
|
||||
return model as U;
|
||||
}
|
||||
|
||||
@@ -126,7 +144,7 @@ export class UniverInstanceService extends Disposable implements IUniverInstance
|
||||
return this.currentUnits$.pipe(map((units) => units.get(type) ?? null), distinctUntilChanged()) as Observable<Nullable<T>>;
|
||||
}
|
||||
|
||||
getCurrentUnitForType<T>(type: UnitType): Nullable<T> {
|
||||
getCurrentUnitForType<T extends UnitModel>(type: UnitType): Nullable<T> {
|
||||
return this._currentUnits.get(type) as Nullable<T>;
|
||||
}
|
||||
|
||||
@@ -138,11 +156,6 @@ export class UniverInstanceService extends Disposable implements IUniverInstance
|
||||
this._currentUnits$.next(this._currentUnits);
|
||||
}
|
||||
|
||||
private _removeCurrentUnitForType(type: UnitType): void {
|
||||
this._currentUnits.set(type, null);
|
||||
this._currentUnits$.next(this._currentUnits);
|
||||
}
|
||||
|
||||
private readonly _unitAdded$ = new Subject<UnitModel>();
|
||||
readonly unitAdded$ = this._unitAdded$.asObservable();
|
||||
getTypeOfUnitAdded$<T extends UnitModel<object, number>>(type: UnitType): Observable<T> {
|
||||
@@ -156,17 +169,25 @@ export class UniverInstanceService extends Disposable implements IUniverInstance
|
||||
*
|
||||
* @param unit The unit to be added.
|
||||
*/
|
||||
__addUnit(unit: UnitModel): void {
|
||||
__addUnit(unit: UnitModel, options?: ICreateUnitOptions): void {
|
||||
const type = unit.type;
|
||||
|
||||
if (!this._unitsByType.has(type)) {
|
||||
this._unitsByType.set(type, []);
|
||||
}
|
||||
this._unitsByType.get(type)!.push(unit);
|
||||
|
||||
const units = this._unitsByType.get(type)!;
|
||||
const newUnitId = unit.getUnitId();
|
||||
if (units.findIndex((u) => u.getUnitId() === newUnitId) !== -1) {
|
||||
throw new Error(`[UniverInstanceService]: cannot create a unit with the same unit id: ${newUnitId}.`);
|
||||
}
|
||||
|
||||
units.push(unit);
|
||||
this._unitAdded$.next(unit);
|
||||
|
||||
this.setCurrentUnitForType(unit.getUnitId());
|
||||
if (options?.makeCurrent ?? true) {
|
||||
this.setCurrentUnitForType(unit.getUnitId());
|
||||
}
|
||||
}
|
||||
|
||||
private _unitDisposed$ = new Subject<UnitModel>();
|
||||
@@ -222,17 +243,28 @@ export class UniverInstanceService extends Disposable implements IUniverInstance
|
||||
this._focused$.next(id);
|
||||
|
||||
if (this.focused instanceof Workbook) {
|
||||
this._contextService.setContextValue(FOCUSING_UNIT, true);
|
||||
this._contextService.setContextValue(FOCUSING_DOC, false);
|
||||
this._contextService.setContextValue(FOCUSING_SHEET, true);
|
||||
this._contextService.setContextValue(FOCUSING_SLIDE, false);
|
||||
this.setCurrentUnitForType(id!);
|
||||
} else if (this.focused instanceof DocumentDataModel) {
|
||||
this._contextService.setContextValue(FOCUSING_UNIT, true);
|
||||
this._contextService.setContextValue(FOCUSING_DOC, true);
|
||||
this._contextService.setContextValue(FOCUSING_SHEET, false);
|
||||
this._contextService.setContextValue(FOCUSING_SLIDE, false);
|
||||
this.setCurrentUnitForType(id!);
|
||||
} else if (this.focused instanceof SlideDataModel) {
|
||||
this._contextService.setContextValue(FOCUSING_UNIT, true);
|
||||
this._contextService.setContextValue(FOCUSING_DOC, false);
|
||||
this._contextService.setContextValue(FOCUSING_SHEET, false);
|
||||
this._contextService.setContextValue(FOCUSING_SLIDE, true);
|
||||
this.setCurrentUnitForType(id!);
|
||||
} else {
|
||||
this._contextService.setContextValue(FOCUSING_UNIT, false);
|
||||
this._contextService.setContextValue(FOCUSING_DOC, false);
|
||||
this._contextService.setContextValue(FOCUSING_SHEET, false);
|
||||
this._contextService.setContextValue(FOCUSING_SLIDE, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,16 +288,28 @@ export class UniverInstanceService extends Disposable implements IUniverInstance
|
||||
const index = units.indexOf(unit);
|
||||
units.splice(index, 1);
|
||||
|
||||
// Firstly un-mark the unit as "current".
|
||||
this._removeCurrentUnitForType(type);
|
||||
this._focused$.next(null);
|
||||
this._tryRemoveCurrentUnitForType(unitId, type);
|
||||
this._tryBlurUnitOnRemoval(unitId);
|
||||
|
||||
// Then dispose the unit.
|
||||
this._unitDisposed$.next(unit);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _tryRemoveCurrentUnitForType(unitId: string, type: UnitType): void {
|
||||
const current = this.getCurrentUnitForType(type);
|
||||
if (current?.getUnitId() === unitId) {
|
||||
this._currentUnits.set(type, null);
|
||||
this._currentUnits$.next(this._currentUnits);
|
||||
}
|
||||
}
|
||||
|
||||
private _tryBlurUnitOnRemoval(unitId: string): void {
|
||||
if (this.focused?.getUnitId() === unitId) {
|
||||
this._focused$.next(null);
|
||||
}
|
||||
};
|
||||
|
||||
private _getUnitById(unitId: string): Nullable<[UnitModel, UnitType]> {
|
||||
for (const [type, units] of this._unitsByType) {
|
||||
const unit = units.find((unit) => unit.getUnitId() === unitId);
|
||||
|
||||
@@ -42,4 +42,4 @@ export interface IResourceManagerService {
|
||||
unloadResources(unitId: string): void;
|
||||
}
|
||||
|
||||
export const IResourceManagerService = createIdentifier<IResourceManagerService>('resource-manager-service');
|
||||
export const IResourceManagerService = createIdentifier<IResourceManagerService>('core.resource-manager.service');
|
||||
|
||||
@@ -35,6 +35,33 @@ import {
|
||||
} from './snapshot-utils';
|
||||
import type { ISnapshotServerService } from './snapshot-server.service';
|
||||
|
||||
export async function generateTempDocumentSnapshot(
|
||||
_context: ILogContext,
|
||||
document: IDocumentData,
|
||||
unitID: string,
|
||||
rev: number
|
||||
) {
|
||||
const documentMeta: IDocumentMeta = {
|
||||
unitID: document.id,
|
||||
rev,
|
||||
creator: '',
|
||||
name: document.title ?? '',
|
||||
resources: document.resources || [],
|
||||
originalMeta: encodeDocOriginalMeta(document),
|
||||
};
|
||||
const snapshot: ISnapshot = {
|
||||
unitID,
|
||||
rev: documentMeta.rev,
|
||||
type: UniverType.UNIVER_DOC,
|
||||
workbook: undefined,
|
||||
doc: documentMeta,
|
||||
};
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateTemporarySnap(
|
||||
context: ILogContext,
|
||||
workbook: IWorkbookData,
|
||||
@@ -375,7 +402,7 @@ export async function getSheetBlocksFromSnapshot(snapshot: ISnapshot, snapshotSe
|
||||
const blocks: ISheetBlock[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
Object.entries(workbookMeta.blockMeta).forEach(([sheetID, blocksOfSheet]) => {
|
||||
Object.entries(workbookMeta.blockMeta).forEach(([_, blocksOfSheet]) => {
|
||||
const blockPromises = blocksOfSheet.blocks.map(async (blockID) => {
|
||||
const params: IGetSheetBlockRequest = {
|
||||
unitID: workbookMeta.unitID,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY } f
|
||||
import { Disposable, toDisposable } from '../../shared/lifecycle';
|
||||
import type { ICommand, IMutationInfo } from '../command/command.service';
|
||||
import { CommandType, ICommandService, sequenceExecute } from '../command/command.service';
|
||||
import { EDITOR_ACTIVATED, FOCUSING_FORMULA_EDITOR, FOCUSING_SHEET } from '../context/context';
|
||||
import { EDITOR_ACTIVATED, FOCUSING_FX_BAR_EDITOR, FOCUSING_SHEET } from '../context/context';
|
||||
import { IContextService } from '../context/context.service';
|
||||
import { IUniverInstanceService } from '../instance/instance.service';
|
||||
import type { Nullable } from '../../common/type-util';
|
||||
@@ -342,7 +342,7 @@ export class LocalUndoRedoService extends Disposable implements IUndoRedoService
|
||||
let unitID: string = '';
|
||||
|
||||
const isFocusSheet = this._contextService.getContextValue(FOCUSING_SHEET);
|
||||
const isFocusFormulaEditor = this._contextService.getContextValue(FOCUSING_FORMULA_EDITOR);
|
||||
const isFocusFormulaEditor = this._contextService.getContextValue(FOCUSING_FX_BAR_EDITOR);
|
||||
const isFocusEditor = this._contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isFocusSheet) {
|
||||
|
||||
@@ -131,6 +131,7 @@ export class Workbook extends UnitModel<IWorkbookData, UniverInstanceType.UNIVER
|
||||
return this._snapshot;
|
||||
}
|
||||
|
||||
/** @deprecated use use name property instead */
|
||||
getName(): string {
|
||||
return this._snapshot.name;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, type Observable } from 'rxjs';
|
||||
import { UnitModel, UniverInstanceType } from '../common/unit';
|
||||
import type { Nullable } from '../shared';
|
||||
import { generateRandomId } from '../shared';
|
||||
import { generateRandomId, Tools } from '../shared';
|
||||
import { DEFAULT_SLIDE } from '../types/const';
|
||||
import type { ISlideData, ISlidePage } from '../types/interfaces';
|
||||
import { PageType } from '../types/interfaces';
|
||||
@@ -42,6 +42,9 @@ export class SlideDataModel extends UnitModel<ISlideData, UniverInstanceType.UNI
|
||||
|
||||
readonly activePage$ = this._activePage$.asObservable();
|
||||
|
||||
private readonly _name$: BehaviorSubject<string>;
|
||||
override name$: Observable<string>;
|
||||
|
||||
private _snapshot: ISlideData;
|
||||
|
||||
private _unitId: string;
|
||||
@@ -50,6 +53,15 @@ export class SlideDataModel extends UnitModel<ISlideData, UniverInstanceType.UNI
|
||||
super();
|
||||
|
||||
this._snapshot = { ...DEFAULT_SLIDE, ...snapshot };
|
||||
this._unitId = this._snapshot.id ?? Tools.generateRandomId(6);
|
||||
|
||||
this._name$ = new BehaviorSubject(this._snapshot.title);
|
||||
this.name$ = this._name$.asObservable();
|
||||
}
|
||||
|
||||
override setName(name: string): void {
|
||||
this._snapshot.title = name;
|
||||
this._name$.next(name);
|
||||
this._unitId = this._snapshot.id ?? generateRandomId(6);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,13 +126,13 @@ export class Univer {
|
||||
|
||||
const univerInstanceService = injector.get(IUniverInstanceService) as UniverInstanceService;
|
||||
univerInstanceService.__setCreateHandler(
|
||||
(type: UnitType, data, ctor) => {
|
||||
(type: UnitType, data, ctor, options) => {
|
||||
if (!this._startedTypes.has(type)) {
|
||||
this._pluginService.startPluginForType(type);
|
||||
this._startedTypes.add(type);
|
||||
|
||||
const model = injector.createInstance(ctor, data);
|
||||
univerInstanceService.__addUnit(model);
|
||||
univerInstanceService.__addUnit(model, options);
|
||||
|
||||
this._tryProgressToReady();
|
||||
|
||||
@@ -140,7 +140,7 @@ export class Univer {
|
||||
}
|
||||
|
||||
const model = injector.createInstance(ctor, data);
|
||||
univerInstanceService.__addUnit(model);
|
||||
univerInstanceService.__addUnit(model, options);
|
||||
return model;
|
||||
}
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user