snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
41
packages/docs-quick-insert-ui/README.md
Normal file
41
packages/docs-quick-insert-ui/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/docs-quick-insert-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/docs-quick-insert-ui)
|
||||
[](https://npmjs.com/package/@univerjs/docs-quick-insert-ui)
|
||||
[](https://npmjs.com/package/@univerjs/docs-quick-insert-ui)
|
||||
|
||||
`@univerjs/docs-quick-insert-ui` adds quick-insert UI capabilities for Univer Docs, including insert affordances backed by the Docs and Drawing packages.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/docs-quick-insert-ui` | `UniverDocsQuickInsertUi` | Yes | Yes | No |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/docs-quick-insert-ui
|
||||
# or
|
||||
npm install @univerjs/docs-quick-insert-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/docs-quick-insert-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/docs-quick-insert-ui/locale/en-US';
|
||||
import { UniverDocsQuickInsertUIPlugin } from '@univerjs/docs-quick-insert-ui';
|
||||
|
||||
univer.registerPlugin(UniverDocsQuickInsertUIPlugin);
|
||||
|
||||
// Merge EnUS into your Univer locale map when this package contributes UI text.
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/docs-quick-insert-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
99
packages/docs-quick-insert-ui/package.json
Normal file
99
packages/docs-quick-insert-ui/package.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"name": "@univerjs/docs-quick-insert-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Quick insert UI integration for Univer Docs.",
|
||||
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"univer",
|
||||
"docs",
|
||||
"quick-insert",
|
||||
"insert",
|
||||
"ui"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./locale/*": {
|
||||
"import": "./lib/es/locale/*.js",
|
||||
"require": "./lib/cjs/locale/*.js",
|
||||
"types": "./lib/types/locale/*.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-drawing": "workspace:*",
|
||||
"@univerjs/docs-drawing-ui": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/drawing-ui": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/docs-quick-insert-ui/postcss.config.mjs
Normal file
1
packages/docs-quick-insert-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ICommandService, Injector, RANGE_DIRECTION } from '@univerjs/core';
|
||||
import { CutContentCommand } from '@univerjs/docs-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DeleteSearchKeyCommand } from '../doc-quick-insert.command';
|
||||
|
||||
class RecordingCommandService {
|
||||
readonly syncExecuteCommand = vi.fn(() => true);
|
||||
}
|
||||
|
||||
function createAccessor(bindings: Array<[unknown, unknown]>) {
|
||||
const injector = new Injector();
|
||||
|
||||
bindings.forEach(([token, useClass]) => {
|
||||
injector.add([token as never, { useClass: useClass as never }]);
|
||||
});
|
||||
|
||||
return injector;
|
||||
}
|
||||
|
||||
describe('DeleteSearchKeyCommand', () => {
|
||||
it('cuts the slash-triggered keyword range from the document', () => {
|
||||
const accessor = createAccessor([
|
||||
[ICommandService, RecordingCommandService],
|
||||
]);
|
||||
const commandService = accessor.get(ICommandService) as unknown as RecordingCommandService;
|
||||
|
||||
const result = DeleteSearchKeyCommand.handler(accessor, { start: 4, end: 9 });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenCalledWith(CutContentCommand.id, {
|
||||
segmentId: '',
|
||||
textRanges: [{
|
||||
startOffset: 4,
|
||||
endOffset: 4,
|
||||
collapsed: true,
|
||||
}],
|
||||
selections: [{
|
||||
startOffset: 4,
|
||||
endOffset: 9,
|
||||
collapsed: false,
|
||||
direction: RANGE_DIRECTION.FORWARD,
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { IInnerCutCommandParams } from '@univerjs/docs-ui';
|
||||
import { CommandType, ICommandService, RANGE_DIRECTION } from '@univerjs/core';
|
||||
import { CutContentCommand } from '@univerjs/docs-ui';
|
||||
|
||||
interface IDeleteSearchKeyCommandParams {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export const DeleteSearchKeyCommand: ICommand<IDeleteSearchKeyCommandParams> = {
|
||||
id: 'doc.command.delete-search-key',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
handler: (accessor, params: IDeleteSearchKeyCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const { start, end } = params;
|
||||
return commandService.syncExecuteCommand(CutContentCommand.id, {
|
||||
segmentId: '',
|
||||
textRanges: [{
|
||||
startOffset: start,
|
||||
endOffset: start,
|
||||
collapsed: true,
|
||||
}],
|
||||
selections: [{
|
||||
startOffset: start,
|
||||
endOffset: end,
|
||||
collapsed: false,
|
||||
direction: RANGE_DIRECTION.FORWARD,
|
||||
}],
|
||||
} as IInnerCutCommandParams);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injector } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DocQuickInsertPopupService } from '../../../services/doc-quick-insert-popup.service';
|
||||
import { CloseQuickInsertPopupOperation, ShowQuickInsertPopupOperation } from '../quick-insert-popup.operation';
|
||||
|
||||
class RecordingQuickInsertPopupService {
|
||||
readonly showPopup = vi.fn();
|
||||
readonly closePopup = vi.fn();
|
||||
}
|
||||
|
||||
function createAccessor(bindings: Array<[unknown, unknown]>) {
|
||||
const injector = new Injector();
|
||||
|
||||
bindings.forEach(([token, useClass]) => {
|
||||
injector.add([token as never, { useClass: useClass as never }]);
|
||||
});
|
||||
|
||||
return injector;
|
||||
}
|
||||
|
||||
describe('quick insert popup operations', () => {
|
||||
it('shows a popup only when parameters are provided', () => {
|
||||
const accessor = createAccessor([
|
||||
[DocQuickInsertPopupService, RecordingQuickInsertPopupService],
|
||||
]);
|
||||
const popupService = accessor.get(DocQuickInsertPopupService) as unknown as RecordingQuickInsertPopupService;
|
||||
const params = {
|
||||
unitId: 'doc-1',
|
||||
index: 3,
|
||||
popup: {
|
||||
keyword: '/',
|
||||
menus$: { subscribe: vi.fn() },
|
||||
},
|
||||
} as never;
|
||||
|
||||
expect(ShowQuickInsertPopupOperation.handler(accessor, undefined)).toBe(false);
|
||||
expect(ShowQuickInsertPopupOperation.handler(accessor, params)).toBe(true);
|
||||
expect(popupService.showPopup).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('closes the active popup session', () => {
|
||||
const accessor = createAccessor([
|
||||
[DocQuickInsertPopupService, RecordingQuickInsertPopupService],
|
||||
]);
|
||||
const popupService = accessor.get(DocQuickInsertPopupService) as unknown as RecordingQuickInsertPopupService;
|
||||
|
||||
expect(CloseQuickInsertPopupOperation.handler(accessor)).toBe(true);
|
||||
expect(popupService.closePopup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { IDocPopup } from '../../services/doc-quick-insert-popup.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { DocQuickInsertPopupService } from '../../services/doc-quick-insert-popup.service';
|
||||
|
||||
export interface IShowQuickInsertPopupOperationParams {
|
||||
unitId: string;
|
||||
index: number;
|
||||
popup: IDocPopup;
|
||||
}
|
||||
|
||||
export const ShowQuickInsertPopupOperation: ICommand<IShowQuickInsertPopupOperationParams> = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'doc.operation.show-quick-insert-popup',
|
||||
handler(accessor, params) {
|
||||
const docQuickInsertPopupService = accessor.get(DocQuickInsertPopupService);
|
||||
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
docQuickInsertPopupService.showPopup(params);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseQuickInsertPopupOperation: ICommand = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'doc.operation.close-quick-insert-popup',
|
||||
handler(accessor) {
|
||||
const docQuickInsertPopupService = accessor.get(DocQuickInsertPopupService);
|
||||
|
||||
docQuickInsertPopupService.closePopup();
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
28
packages/docs-quick-insert-ui/src/config/config.ts
Normal file
28
packages/docs-quick-insert-ui/src/config/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const DOCS_QUICK_INSERT_UI_PLUGIN_CONFIG_KEY = 'docs-quick-insert-ui.config';
|
||||
export const DOCS_QUICK_INSERT_UI_PLUGIN_NAME = 'DOC_QUICK_INSERT_UI_PLUGIN';
|
||||
|
||||
export const configSymbol = Symbol(DOCS_QUICK_INSERT_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverDocsQuickInsertUIConfig {
|
||||
menu?: MenuConfig;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverDocsQuickInsertUIConfig = {};
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeleteDirection, Direction } from '@univerjs/core';
|
||||
import { DeleteTextCommand, InsertTextCommand, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DeleteLeftCommand, IMEInputCommand, MoveCursorOperation } from '@univerjs/docs-ui';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CloseQuickInsertPopupOperation, ShowQuickInsertPopupOperation } from '../../commands/operations/quick-insert-popup.operation';
|
||||
import { numberedListMenu, textMenu } from '../../menu/menu';
|
||||
import { DocQuickInsertTriggerController } from '../doc-quick-insert-trigger.controller';
|
||||
|
||||
function createCommandService() {
|
||||
const listeners = new Set<(commandInfo: { id: string; params?: unknown }) => void>();
|
||||
|
||||
return {
|
||||
executeCommand: vi.fn(() => true),
|
||||
onCommandExecuted: vi.fn((callback: (commandInfo: { id: string; params?: unknown }) => void) => {
|
||||
listeners.add(callback);
|
||||
return {
|
||||
dispose: () => listeners.delete(callback),
|
||||
};
|
||||
}),
|
||||
emit(commandInfo: { id: string; params?: unknown }) {
|
||||
listeners.forEach((listener) => listener(commandInfo));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPopupService() {
|
||||
let menuSelectedHandler: ((menu: { id: string }) => void) | null = null;
|
||||
let editPopup: { anchor: number } | null = null;
|
||||
let isComposing = false;
|
||||
let inputOffset = { start: 0, end: 0 };
|
||||
|
||||
return {
|
||||
get editPopup() {
|
||||
return editPopup;
|
||||
},
|
||||
setEditPopup(value: { anchor: number } | null) {
|
||||
editPopup = value;
|
||||
},
|
||||
get isComposing() {
|
||||
return isComposing;
|
||||
},
|
||||
get inputOffset() {
|
||||
return inputOffset;
|
||||
},
|
||||
setInputOffset: vi.fn((value: { start: number; end: number }) => {
|
||||
inputOffset = value;
|
||||
}),
|
||||
resolvePopup: vi.fn(),
|
||||
setIsComposing: vi.fn((value: boolean) => {
|
||||
isComposing = value;
|
||||
}),
|
||||
onMenuSelected: vi.fn((handler: (menu: { id: string }) => void) => {
|
||||
menuSelectedHandler = handler;
|
||||
return () => {
|
||||
menuSelectedHandler = null;
|
||||
};
|
||||
}),
|
||||
triggerMenuSelected(menu: { id: string }) {
|
||||
menuSelectedHandler?.(menu);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('DocQuickInsertTriggerController', () => {
|
||||
it('reacts to insert, edit, cursor, delete and IME events like a real quick-insert session', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const commandService = createCommandService();
|
||||
const popupService = createPopupService();
|
||||
const shortcutService = {
|
||||
registerShortcut: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
};
|
||||
const selectionManagerService = {
|
||||
getActiveTextRange: vi.fn(() => ({ startOffset: 5, endOffset: 5 })),
|
||||
};
|
||||
const popup = {
|
||||
keyword: '/',
|
||||
menus$: { subscribe: vi.fn() },
|
||||
preconditions: vi.fn(() => true),
|
||||
};
|
||||
popupService.resolvePopup.mockReturnValue(popup);
|
||||
|
||||
const controller = new DocQuickInsertTriggerController(
|
||||
commandService as never,
|
||||
selectionManagerService as never,
|
||||
popupService as never,
|
||||
shortcutService as never,
|
||||
{
|
||||
getCurrentUnitOfType: vi.fn(() => ({ getDisabled: () => false })),
|
||||
} as never
|
||||
);
|
||||
|
||||
expect(shortcutService.registerShortcut).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: CloseQuickInsertPopupOperation.id,
|
||||
binding: KeyCode.ESC,
|
||||
priority: 1000,
|
||||
}));
|
||||
|
||||
commandService.emit({
|
||||
id: InsertTextCommand.id,
|
||||
params: {
|
||||
body: { dataStream: '/' },
|
||||
range: { endOffset: 5 },
|
||||
unitId: 'doc-1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(popupService.setInputOffset).toHaveBeenCalledWith({ start: 4, end: 5 });
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalledWith(ShowQuickInsertPopupOperation.id, expect.anything());
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(ShowQuickInsertPopupOperation.id, {
|
||||
index: 4,
|
||||
unitId: 'doc-1',
|
||||
popup,
|
||||
});
|
||||
|
||||
popupService.setEditPopup({ anchor: 4 });
|
||||
commandService.emit({
|
||||
id: InsertTextCommand.id,
|
||||
params: {
|
||||
body: { dataStream: 'a' },
|
||||
range: { endOffset: 7 },
|
||||
unitId: 'doc-1',
|
||||
},
|
||||
});
|
||||
expect(popupService.setInputOffset).toHaveBeenLastCalledWith({ start: 4, end: 8 });
|
||||
|
||||
commandService.emit({
|
||||
id: IMEInputCommand.id,
|
||||
params: { isCompositionStart: true, isCompositionEnd: false },
|
||||
});
|
||||
commandService.emit({
|
||||
id: IMEInputCommand.id,
|
||||
params: { isCompositionStart: false, isCompositionEnd: true },
|
||||
});
|
||||
expect(popupService.setIsComposing).toHaveBeenNthCalledWith(1, true);
|
||||
expect(popupService.setIsComposing).toHaveBeenNthCalledWith(2, false);
|
||||
|
||||
commandService.emit({
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
isCompositionEnd: true,
|
||||
textRanges: [{ endOffset: 9 }],
|
||||
},
|
||||
});
|
||||
expect(popupService.setInputOffset).toHaveBeenLastCalledWith({ start: 4, end: 9 });
|
||||
|
||||
commandService.emit({
|
||||
id: DeleteTextCommand.id,
|
||||
params: {
|
||||
direction: DeleteDirection.LEFT,
|
||||
len: 2,
|
||||
range: { endOffset: 10 },
|
||||
},
|
||||
});
|
||||
expect(popupService.setInputOffset).toHaveBeenLastCalledWith({ start: 4, end: 8 });
|
||||
|
||||
commandService.emit({
|
||||
id: MoveCursorOperation.id,
|
||||
params: { direction: Direction.LEFT },
|
||||
});
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(CloseQuickInsertPopupOperation.id);
|
||||
|
||||
selectionManagerService.getActiveTextRange.mockReturnValue({ startOffset: 4, endOffset: 4 });
|
||||
commandService.emit({ id: DeleteLeftCommand.id });
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(CloseQuickInsertPopupOperation.id);
|
||||
|
||||
controller.dispose();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('executes built-in menu commands but skips plain text selection', () => {
|
||||
const commandService = createCommandService();
|
||||
const popupService = createPopupService();
|
||||
|
||||
const controller = new DocQuickInsertTriggerController(
|
||||
commandService as never,
|
||||
{ getActiveTextRange: vi.fn() } as never,
|
||||
popupService as never,
|
||||
{ registerShortcut: vi.fn(() => ({ dispose: vi.fn() })) } as never,
|
||||
{
|
||||
getCurrentUnitOfType: vi.fn(() => ({ getDisabled: () => false })),
|
||||
} as never
|
||||
);
|
||||
|
||||
popupService.triggerMenuSelected(textMenu);
|
||||
popupService.triggerMenuSelected(numberedListMenu);
|
||||
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalledWith(textMenu.id);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(numberedListMenu.id);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { DividerIcon, TextIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { KeywordInputPlaceholder } from '../views/KeywordInputPlaceholder';
|
||||
import { QuickInsertButton } from '../views/QuickInsertButton';
|
||||
import { QuickInsertPlaceholder } from '../views/QuickInsertPlaceholder';
|
||||
import { QuickInsertPopup } from '../views/QuickInsertPopup';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) protected readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
DividerIcon,
|
||||
TextIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
protected _registerComponents(): void {
|
||||
this._registerPopup();
|
||||
([
|
||||
[KeywordInputPlaceholder.componentKey, KeywordInputPlaceholder],
|
||||
[QuickInsertPlaceholder.componentKey, QuickInsertPlaceholder],
|
||||
[QuickInsertButton.componentKey, QuickInsertButton],
|
||||
] as const).forEach(([key, comp]) => {
|
||||
if (key) {
|
||||
this.disposeWithMe(this._componentManager.register(key, comp));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _registerPopup(): void {
|
||||
this.disposeWithMe(this._componentManager.register(QuickInsertPopup.componentKey, QuickInsertPopup));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel } from '@univerjs/core';
|
||||
import type {
|
||||
IDeleteTextCommandParams,
|
||||
IInsertTextCommandParams,
|
||||
IRichTextEditingMutationParams,
|
||||
} from '@univerjs/docs';
|
||||
import type { IIMEInputCommandParams, IMoveCursorOperationParams } from '@univerjs/docs-ui';
|
||||
import {
|
||||
DeleteDirection,
|
||||
Direction,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DeleteTextCommand,
|
||||
DocSelectionManagerService,
|
||||
InsertTextCommand,
|
||||
RichTextEditingMutation,
|
||||
} from '@univerjs/docs';
|
||||
import { DeleteLeftCommand, IMEInputCommand, MoveCursorOperation } from '@univerjs/docs-ui';
|
||||
import { IShortcutService, KeyCode } from '@univerjs/ui';
|
||||
import {
|
||||
CloseQuickInsertPopupOperation,
|
||||
ShowQuickInsertPopupOperation,
|
||||
} from '../commands/operations/quick-insert-popup.operation';
|
||||
import { builtInMenuCommandIds, textMenu } from '../menu/menu';
|
||||
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
|
||||
|
||||
export class DocQuickInsertTriggerController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@Inject(DocQuickInsertPopupService) private readonly _docQuickInsertPopupService: DocQuickInsertPopupService,
|
||||
@Inject(IShortcutService) private readonly _shortcutService: IShortcutService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut({
|
||||
id: CloseQuickInsertPopupOperation.id,
|
||||
binding: KeyCode.ESC,
|
||||
preconditions: () => Boolean(this._docQuickInsertPopupService.editPopup),
|
||||
priority: 1000,
|
||||
}));
|
||||
|
||||
this._initTrigger();
|
||||
this._initMenuHandler();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initTrigger() {
|
||||
this.disposeWithMe(
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
this._commandService.onCommandExecuted((commandInfo) => {
|
||||
const { _docQuickInsertPopupService, _textSelectionManagerService, _commandService } = this;
|
||||
const documentDataModel = this._univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (documentDataModel?.getDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandInfo.id === InsertTextCommand.id) {
|
||||
const params = commandInfo.params as IInsertTextCommandParams;
|
||||
if (_docQuickInsertPopupService.editPopup) {
|
||||
_docQuickInsertPopupService.setInputOffset({
|
||||
start: _docQuickInsertPopupService.inputOffset.start,
|
||||
end: params.range.endOffset + 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRange = _textSelectionManagerService.getActiveTextRange();
|
||||
if (!activeRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popup = _docQuickInsertPopupService.resolvePopup(params.body.dataStream);
|
||||
if (!popup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const available = popup.preconditions ? popup.preconditions(params) : true;
|
||||
if (!available) {
|
||||
return;
|
||||
}
|
||||
|
||||
_docQuickInsertPopupService.setInputOffset({ start: activeRange.startOffset - 1, end: activeRange.startOffset });
|
||||
|
||||
setTimeout(() => {
|
||||
_commandService.executeCommand(ShowQuickInsertPopupOperation.id, {
|
||||
index: activeRange.startOffset - 1,
|
||||
unitId: params.unitId,
|
||||
popup,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (commandInfo.id === IMEInputCommand.id) {
|
||||
const params = commandInfo.params as IIMEInputCommandParams;
|
||||
if (!_docQuickInsertPopupService.isComposing && params.isCompositionStart) {
|
||||
_docQuickInsertPopupService.setIsComposing(true);
|
||||
}
|
||||
|
||||
if (_docQuickInsertPopupService.isComposing && params.isCompositionEnd) {
|
||||
_docQuickInsertPopupService.setIsComposing(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (commandInfo.id === RichTextEditingMutation.id) {
|
||||
const params = commandInfo.params as IRichTextEditingMutationParams;
|
||||
if (params.isCompositionEnd) {
|
||||
const endOffset = params.textRanges?.[0]?.endOffset;
|
||||
if (endOffset) {
|
||||
_docQuickInsertPopupService.setInputOffset({ start: _docQuickInsertPopupService.inputOffset.start, end: endOffset });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commandInfo.id === DeleteTextCommand.id) {
|
||||
const params = commandInfo.params as IDeleteTextCommandParams;
|
||||
if (_docQuickInsertPopupService.editPopup && params.direction === DeleteDirection.LEFT) {
|
||||
const len = params.len ?? 0;
|
||||
_docQuickInsertPopupService.setInputOffset({ start: _docQuickInsertPopupService.inputOffset.start, end: params.range.endOffset - len });
|
||||
}
|
||||
}
|
||||
|
||||
if (commandInfo.id === MoveCursorOperation.id) {
|
||||
const params = commandInfo.params as IMoveCursorOperationParams;
|
||||
|
||||
if (params.direction === Direction.LEFT || params.direction === Direction.RIGHT) {
|
||||
_docQuickInsertPopupService.editPopup && _commandService.executeCommand(CloseQuickInsertPopupOperation.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (commandInfo.id === DeleteLeftCommand.id) {
|
||||
const activeRange = _textSelectionManagerService.getActiveTextRange();
|
||||
if (!_docQuickInsertPopupService.editPopup || !activeRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeRange.endOffset <= _docQuickInsertPopupService.editPopup.anchor) {
|
||||
_commandService.executeCommand(CloseQuickInsertPopupOperation.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initMenuHandler() {
|
||||
this.disposeWithMe(this._docQuickInsertPopupService.onMenuSelected((menu) => {
|
||||
if (menu.id === textMenu.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (builtInMenuCommandIds.has(menu.id)) {
|
||||
this._commandService.executeCommand(menu.id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { DividerIcon, TextIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { KeywordInputPlaceholder } from '../../views/KeywordInputPlaceholder';
|
||||
import { MobileQuickInsertPopup } from '../../views/mobile/MobileQuickInsertPopup';
|
||||
import { QuickInsertButton } from '../../views/QuickInsertButton';
|
||||
import { QuickInsertPlaceholder } from '../../views/QuickInsertPlaceholder';
|
||||
import { QuickInsertPopup } from '../../views/QuickInsertPopup';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) protected readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
DividerIcon,
|
||||
TextIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
protected _registerComponents(): void {
|
||||
this._registerPopup();
|
||||
([
|
||||
[KeywordInputPlaceholder.componentKey, KeywordInputPlaceholder],
|
||||
[QuickInsertPlaceholder.componentKey, QuickInsertPlaceholder],
|
||||
[QuickInsertButton.componentKey, QuickInsertButton],
|
||||
] as const).forEach(([key, comp]) => {
|
||||
if (key) {
|
||||
this.disposeWithMe(this._componentManager.register(key, comp));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _registerPopup(): void {
|
||||
this.disposeWithMe(this._componentManager.register(QuickInsertPopup.componentKey, MobileQuickInsertPopup));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
import type { IDocPopup } from '../services/doc-quick-insert-popup.service';
|
||||
import { Disposable, ICommandService, Inject } from '@univerjs/core';
|
||||
import { of } from 'rxjs';
|
||||
import { DeleteSearchKeyCommand } from '../commands/commands/doc-quick-insert.command';
|
||||
import { CloseQuickInsertPopupOperation, ShowQuickInsertPopupOperation } from '../commands/operations/quick-insert-popup.operation';
|
||||
import { builtInMenus } from '../menu/menu';
|
||||
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
|
||||
|
||||
export class DocQuickInsertUIController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(DocQuickInsertPopupService) private readonly _docQuickInsertPopupService: DocQuickInsertPopupService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initComponents();
|
||||
this._initMenus();
|
||||
}
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
DeleteSearchKeyCommand,
|
||||
ShowQuickInsertPopupOperation,
|
||||
CloseQuickInsertPopupOperation,
|
||||
].forEach((operation) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(operation));
|
||||
});
|
||||
}
|
||||
|
||||
private _initComponents() {
|
||||
const popups: IDocPopup[] = [
|
||||
{
|
||||
keyword: '/',
|
||||
menus$: of(builtInMenus),
|
||||
// only show when the cursor is at the beginning of a line
|
||||
preconditions: (params) => (params.range as ITextRangeWithStyle).startNodePosition?.glyph === 0,
|
||||
},
|
||||
];
|
||||
|
||||
popups.forEach((popup) => {
|
||||
this.disposeWithMe(this._docQuickInsertPopupService.registerPopup(popup));
|
||||
});
|
||||
}
|
||||
|
||||
private _initMenus() {
|
||||
|
||||
}
|
||||
}
|
||||
2
packages/docs-quick-insert-ui/src/global.css
Normal file
2
packages/docs-quick-insert-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
27
packages/docs-quick-insert-ui/src/index.ts
Normal file
27
packages/docs-quick-insert-ui/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './global.css';
|
||||
|
||||
export type { IUniverDocsQuickInsertUIConfig } from './config/config';
|
||||
export { DocQuickInsertTriggerController } from './controllers/doc-quick-insert-trigger.controller';
|
||||
export { DocQuickInsertUIController } from './controllers/ui.controller';
|
||||
export { UniverDocsQuickInsertMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverDocsQuickInsertUIPlugin } from './plugin';
|
||||
export type { DocPopupMenu, IDocPopup, IDocPopupGroupItem, IDocPopupMenuItem } from './services/doc-quick-insert-popup.service';
|
||||
export { DocQuickInsertPopupService } from './services/doc-quick-insert-popup.service';
|
||||
export { KeywordInputPlaceholderComponentKey } from './views/KeywordInputPlaceholder';
|
||||
export { QuickInsertPlaceholderComponentKey } from './views/QuickInsertPlaceholder';
|
||||
37
packages/docs-quick-insert-ui/src/locale/ar-SA.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'قائمة مرقمة',
|
||||
bulletedList: 'قائمة نقطية',
|
||||
divider: 'فاصل',
|
||||
text: 'نص',
|
||||
table: 'جدول',
|
||||
image: 'صورة',
|
||||
},
|
||||
group: {
|
||||
basics: 'أساسيات',
|
||||
},
|
||||
placeholder: 'لم يُعثر على نتائج',
|
||||
keywordInputPlaceholder: 'أدخل الكلمات المفتاحية',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/ca-ES.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Llista numerada',
|
||||
bulletedList: 'Llista amb vinyetes',
|
||||
divider: 'Divisor',
|
||||
text: 'Text',
|
||||
table: 'Taula',
|
||||
image: 'Imatge',
|
||||
},
|
||||
group: {
|
||||
basics: 'Bàsics',
|
||||
},
|
||||
placeholder: 'No s\'han trobat resultats',
|
||||
keywordInputPlaceholder: 'Introdueix paraules clau',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/de-DE.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Nummerierte Liste',
|
||||
bulletedList: 'Aufzählungsliste',
|
||||
divider: 'Trennlinie',
|
||||
text: 'Text',
|
||||
table: 'Tabelle',
|
||||
image: 'Bild',
|
||||
},
|
||||
group: {
|
||||
basics: 'Grundlagen',
|
||||
},
|
||||
placeholder: 'Keine Ergebnisse gefunden',
|
||||
keywordInputPlaceholder: 'Stichwörter eingeben',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
35
packages/docs-quick-insert-ui/src/locale/en-US.ts
Normal file
35
packages/docs-quick-insert-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Numbered List',
|
||||
bulletedList: 'Bulleted List',
|
||||
divider: 'Divider',
|
||||
text: 'Text',
|
||||
table: 'Table',
|
||||
image: 'Image',
|
||||
},
|
||||
group: {
|
||||
basics: 'Basics',
|
||||
},
|
||||
placeholder: 'No results found',
|
||||
keywordInputPlaceholder: 'Enter keywords',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/es-ES.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Lista numerada',
|
||||
bulletedList: 'Lista con viñetas',
|
||||
divider: 'Divisor',
|
||||
text: 'Texto',
|
||||
table: 'Tabla',
|
||||
image: 'Imagen',
|
||||
},
|
||||
group: {
|
||||
basics: 'Básicos',
|
||||
},
|
||||
placeholder: 'No se han encontrado resultados',
|
||||
keywordInputPlaceholder: 'Introduce palabras clave',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/fa-IR.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'لیست شمارهدار',
|
||||
bulletedList: 'لیست نقطهدار',
|
||||
divider: 'خط تقسیم',
|
||||
text: 'متن',
|
||||
table: 'جدول',
|
||||
image: 'تصویر',
|
||||
},
|
||||
group: {
|
||||
basics: 'بنیادی',
|
||||
},
|
||||
placeholder: 'هیچ نتیجهای یافت نشد',
|
||||
keywordInputPlaceholder: 'وارد کلمات کلیدی',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/fr-FR.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Liste numérotée',
|
||||
bulletedList: 'Liste à puces',
|
||||
divider: 'Ligne de séparation',
|
||||
text: 'Texte',
|
||||
table: 'Tableau',
|
||||
image: 'Image',
|
||||
},
|
||||
group: {
|
||||
basics: 'Basiques',
|
||||
},
|
||||
placeholder: 'Aucun résultat',
|
||||
keywordInputPlaceholder: 'Entrez des mots clés',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/id-ID.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Daftar Bernomor',
|
||||
bulletedList: 'Daftar Berpoin',
|
||||
divider: 'Pembatas',
|
||||
text: 'Teks',
|
||||
table: 'Tabel',
|
||||
image: 'Gambar',
|
||||
},
|
||||
group: {
|
||||
basics: 'Dasar',
|
||||
},
|
||||
placeholder: 'Tidak ada hasil ditemukan',
|
||||
keywordInputPlaceholder: 'Masukkan kata kunci',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/it-IT.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Elenco Numerato',
|
||||
bulletedList: 'Elenco Puntato',
|
||||
divider: 'Divisore',
|
||||
text: 'Testo',
|
||||
table: 'Tabella',
|
||||
image: 'Immagine',
|
||||
},
|
||||
group: {
|
||||
basics: 'Elementi Base',
|
||||
},
|
||||
placeholder: 'Nessun risultato trovato',
|
||||
keywordInputPlaceholder: 'Inserisci parole chiave',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/ja-JP.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: '番号付きリスト',
|
||||
bulletedList: '箇条書き',
|
||||
divider: '区切り線',
|
||||
text: 'テキスト',
|
||||
table: '表',
|
||||
image: '画像',
|
||||
},
|
||||
group: {
|
||||
basics: '基本',
|
||||
},
|
||||
placeholder: '結果がありません',
|
||||
keywordInputPlaceholder: 'キーワードを入力',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/ko-KR.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: '번호가 매겨진 목록',
|
||||
bulletedList: '글머리 기호 목록',
|
||||
divider: '구분선',
|
||||
text: '텍스트',
|
||||
table: '표',
|
||||
image: '이미지',
|
||||
},
|
||||
group: {
|
||||
basics: '기본',
|
||||
},
|
||||
placeholder: '결과가 없습니다',
|
||||
keywordInputPlaceholder: '키워드 입력',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/pl-PL.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Lista numerowana',
|
||||
bulletedList: 'Lista punktowana',
|
||||
divider: 'Separator',
|
||||
text: 'Tekst',
|
||||
table: 'Tabela',
|
||||
image: 'Obraz',
|
||||
},
|
||||
group: {
|
||||
basics: 'Podstawy',
|
||||
},
|
||||
placeholder: 'Nie znaleziono wyników',
|
||||
keywordInputPlaceholder: 'Wpisz słowa kluczowe',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/pt-BR.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Lista Numerada',
|
||||
bulletedList: 'Lista com Marcadores',
|
||||
divider: 'Divisória',
|
||||
text: 'Texto',
|
||||
table: 'Tabela',
|
||||
image: 'Imagem',
|
||||
},
|
||||
group: {
|
||||
basics: 'Básicos',
|
||||
},
|
||||
placeholder: 'Nenhum resultado encontrado',
|
||||
keywordInputPlaceholder: 'Digite palavras-chave',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/ru-RU.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Список с нумерацией',
|
||||
bulletedList: 'Маркированный список',
|
||||
divider: 'Разделительная линия',
|
||||
text: 'Текст',
|
||||
table: 'Таблица',
|
||||
image: 'Изображение',
|
||||
},
|
||||
group: {
|
||||
basics: 'Основные',
|
||||
},
|
||||
placeholder: 'Нет результатов',
|
||||
keywordInputPlaceholder: 'Введите ключевые слова',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/sk-SK.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Číslovaný zoznam',
|
||||
bulletedList: 'Odrážkový zoznam',
|
||||
divider: 'Oddeľovač',
|
||||
text: 'Text',
|
||||
table: 'Tabuľka',
|
||||
image: 'Obrázok',
|
||||
},
|
||||
group: {
|
||||
basics: 'Základy',
|
||||
},
|
||||
placeholder: 'Nenašli sa žiadne výsledky',
|
||||
keywordInputPlaceholder: 'Zadajte kľúčové slová',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/docs-quick-insert-ui/src/locale/types.ts
Normal file
20
packages/docs-quick-insert-ui/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
37
packages/docs-quick-insert-ui/src/locale/vi-VN.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: 'Danh sách được đánh số',
|
||||
bulletedList: 'Danh sách không được đánh số',
|
||||
divider: 'Đường phân cách',
|
||||
text: 'Văn bản',
|
||||
table: 'Bảng',
|
||||
image: 'Hình ảnh',
|
||||
},
|
||||
group: {
|
||||
basics: 'Cơ bản',
|
||||
},
|
||||
placeholder: 'Không có kết quả',
|
||||
keywordInputPlaceholder: 'Nhập từ khóa',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/zh-CN.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: '有序列表',
|
||||
bulletedList: '无序列表',
|
||||
divider: '分隔线',
|
||||
text: '文本',
|
||||
table: '表格',
|
||||
image: '图片',
|
||||
},
|
||||
group: {
|
||||
basics: '基础',
|
||||
},
|
||||
placeholder: '暂无结果',
|
||||
keywordInputPlaceholder: '输入关键词',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/zh-HK.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: '有序列表',
|
||||
bulletedList: '無序列表',
|
||||
divider: '分隔線',
|
||||
text: '文字',
|
||||
table: '表格',
|
||||
image: '圖片',
|
||||
},
|
||||
group: {
|
||||
basics: '基礎',
|
||||
},
|
||||
placeholder: '無結果',
|
||||
keywordInputPlaceholder: '輸入關鍵詞',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
37
packages/docs-quick-insert-ui/src/locale/zh-TW.ts
Normal file
37
packages/docs-quick-insert-ui/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-quick-insert-ui': {
|
||||
menu: {
|
||||
numberedList: '有序列表',
|
||||
bulletedList: '無序列表',
|
||||
divider: '分隔線',
|
||||
text: '文字',
|
||||
table: '表格',
|
||||
image: '圖片',
|
||||
},
|
||||
group: {
|
||||
basics: '基礎',
|
||||
},
|
||||
placeholder: '無結果',
|
||||
keywordInputPlaceholder: '輸入關鍵詞',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DocQuickInsertMenuController } from '../doc-quick-insert-menu.controller';
|
||||
|
||||
function createParagraph(startIndex: number, paragraphEnd = startIndex) {
|
||||
return {
|
||||
startIndex,
|
||||
paragraphStart: startIndex,
|
||||
paragraphEnd,
|
||||
firstLine: { left: 1, top: 2, right: 10, bottom: 12 },
|
||||
};
|
||||
}
|
||||
|
||||
describe('DocQuickInsertMenuController', () => {
|
||||
it('shows the quick insert button for empty paragraphs and hides it when the line is no longer eligible', () => {
|
||||
const hoverParagraphLeftRealTime$ = new BehaviorSubject<ReturnType<typeof createParagraph> | null>(null);
|
||||
const hoverParagraphRealTime$ = new BehaviorSubject<ReturnType<typeof createParagraph> | null>(null);
|
||||
const firstDisposable = { dispose: vi.fn(), canDispose: vi.fn(() => true) };
|
||||
const secondDisposable = { dispose: vi.fn(), canDispose: vi.fn(() => true) };
|
||||
const attachPopupToRect = vi.fn()
|
||||
.mockReturnValueOnce(firstDisposable)
|
||||
.mockReturnValueOnce(secondDisposable);
|
||||
|
||||
const controller = new DocQuickInsertMenuController(
|
||||
{
|
||||
unit: {
|
||||
getDisabled: () => false,
|
||||
getUnitId: () => 'doc-1',
|
||||
},
|
||||
} as never,
|
||||
{ hoverParagraphLeftRealTime$, hoverParagraphRealTime$ } as never,
|
||||
{ editPopup: null } as never,
|
||||
{ attachPopupToRect } as never
|
||||
);
|
||||
|
||||
const emptyParagraph = createParagraph(6);
|
||||
hoverParagraphRealTime$.next(emptyParagraph);
|
||||
|
||||
expect(attachPopupToRect).toHaveBeenCalledTimes(1);
|
||||
expect(controller.popup).toEqual({ startIndex: 6, disposable: firstDisposable });
|
||||
|
||||
hoverParagraphRealTime$.next(emptyParagraph);
|
||||
expect(attachPopupToRect).toHaveBeenCalledTimes(1);
|
||||
|
||||
hoverParagraphRealTime$.next(createParagraph(6, 9));
|
||||
expect(firstDisposable.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(controller.popup).toBeNull();
|
||||
|
||||
hoverParagraphLeftRealTime$.next(createParagraph(8));
|
||||
expect(attachPopupToRect).toHaveBeenCalledTimes(2);
|
||||
expect(controller.popup).toEqual({ startIndex: 8, disposable: secondDisposable });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps the left-side button mounted while the edit popup itself is active', () => {
|
||||
const hoverParagraphLeftRealTime$ = new BehaviorSubject<ReturnType<typeof createParagraph> | null>(createParagraph(3));
|
||||
const hoverParagraphRealTime$ = new BehaviorSubject<ReturnType<typeof createParagraph> | null>(null);
|
||||
const disposable = { dispose: vi.fn(), canDispose: vi.fn(() => true) };
|
||||
const quickInsertPopupService = {
|
||||
editPopup: null as { anchor: number } | null,
|
||||
};
|
||||
|
||||
const controller = new DocQuickInsertMenuController(
|
||||
{
|
||||
unit: {
|
||||
getDisabled: () => false,
|
||||
getUnitId: () => 'doc-1',
|
||||
},
|
||||
} as never,
|
||||
{ hoverParagraphLeftRealTime$, hoverParagraphRealTime$ } as never,
|
||||
quickInsertPopupService as never,
|
||||
{ attachPopupToRect: vi.fn(() => disposable) } as never
|
||||
);
|
||||
|
||||
quickInsertPopupService.editPopup = { anchor: 3 };
|
||||
hoverParagraphLeftRealTime$.next(null);
|
||||
hoverParagraphRealTime$.next(null);
|
||||
|
||||
expect(disposable.dispose).not.toHaveBeenCalled();
|
||||
expect(controller.popup).toEqual({ startIndex: 3, disposable });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, INeedCheckDisposable, Nullable } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { DocCanvasPopManagerService, DocEventManagerService } from '@univerjs/docs-ui';
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
|
||||
import { QuickInsertButtonComponentKey } from '../views/QuickInsertButton';
|
||||
|
||||
export class DocQuickInsertMenuController extends Disposable implements IRenderModule {
|
||||
private _popup$ = new BehaviorSubject<Nullable<{ startIndex: number; disposable: INeedCheckDisposable }>>(null);
|
||||
readonly popup$ = this._popup$.asObservable();
|
||||
get popup() {
|
||||
return this._popup$.value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _context: IRenderContext<DocumentDataModel>,
|
||||
@Inject(DocEventManagerService) private _docEventManagerService: DocEventManagerService,
|
||||
@Inject(DocQuickInsertPopupService) private _docQuickInsertPopupService: DocQuickInsertPopupService,
|
||||
@Inject(DocCanvasPopManagerService) private _docCanvasPopManagerService: DocCanvasPopManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this.disposeWithMe(combineLatest([this._docEventManagerService.hoverParagraphLeftRealTime$, this._docEventManagerService.hoverParagraphRealTime$]).subscribe(([left, paragraph]) => {
|
||||
const p = left ?? paragraph;
|
||||
const isDisabled = this._context.unit.getDisabled();
|
||||
if (!p || isDisabled) {
|
||||
this._hideMenu(true);
|
||||
return;
|
||||
}
|
||||
if (p.paragraphStart === p.paragraphEnd) {
|
||||
if (this._docQuickInsertPopupService.editPopup || p.startIndex === this.popup?.startIndex) return;
|
||||
this._hideMenu(true);
|
||||
const disposable = this._docCanvasPopManagerService.attachPopupToRect(p.firstLine, {
|
||||
componentKey: QuickInsertButtonComponentKey,
|
||||
direction: 'left-center',
|
||||
}, this._context.unit.getUnitId());
|
||||
|
||||
this._popup$.next({
|
||||
startIndex: p.startIndex,
|
||||
disposable,
|
||||
});
|
||||
} else {
|
||||
this._hideMenu(true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _hideMenu(force: boolean) {
|
||||
if (this._docQuickInsertPopupService.editPopup) return;
|
||||
if (this.popup && (force || this.popup.disposable.canDispose())) {
|
||||
this.popup.disposable.dispose();
|
||||
this._popup$.next(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
packages/docs-quick-insert-ui/src/menu/menu.ts
Normal file
94
packages/docs-quick-insert-ui/src/menu/menu.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocPopupMenu, IDocPopupMenuItem } from '../services/doc-quick-insert-popup.service';
|
||||
import { InsertDocImageCommand } from '@univerjs/docs-drawing-ui';
|
||||
import {
|
||||
BulletListCommand,
|
||||
DocCreateTableOperation,
|
||||
HorizontalLineCommand,
|
||||
OrderListCommand,
|
||||
} from '@univerjs/docs-ui';
|
||||
|
||||
export enum QuickInsertMenuGroup {
|
||||
Basic = 'quick.insert.group.basic',
|
||||
Media = 'quick.insert.group.media',
|
||||
}
|
||||
|
||||
export const textMenu: IDocPopupMenuItem = {
|
||||
id: 'quick-insert.text.menu',
|
||||
title: 'docs-quick-insert-ui.menu.text',
|
||||
icon: 'TextIcon',
|
||||
keywords: ['text'],
|
||||
};
|
||||
|
||||
export const numberedListMenu: IDocPopupMenuItem = {
|
||||
id: OrderListCommand.id,
|
||||
title: 'docs-quick-insert-ui.menu.numberedList',
|
||||
icon: 'OrderIcon',
|
||||
keywords: ['numbered', 'list', 'ordered'],
|
||||
};
|
||||
|
||||
export const bulletedListMenu: IDocPopupMenuItem = {
|
||||
id: BulletListCommand.id,
|
||||
title: 'docs-quick-insert-ui.menu.bulletedList',
|
||||
icon: 'UnorderIcon',
|
||||
keywords: ['bulleted', 'list', 'unordered'],
|
||||
};
|
||||
|
||||
export const dividerMenu: IDocPopupMenuItem = {
|
||||
id: HorizontalLineCommand.id,
|
||||
title: 'docs-quick-insert-ui.menu.divider',
|
||||
icon: 'DividerIcon',
|
||||
keywords: ['divider', 'line', 'separate'],
|
||||
};
|
||||
|
||||
export const tableMenu: IDocPopupMenuItem = {
|
||||
id: DocCreateTableOperation.id,
|
||||
title: 'docs-quick-insert-ui.menu.table',
|
||||
icon: 'GridIcon',
|
||||
keywords: ['table', 'grid', 'spreadsheet'],
|
||||
};
|
||||
|
||||
export const imageMenu: IDocPopupMenuItem = {
|
||||
id: InsertDocImageCommand.id,
|
||||
title: 'docs-quick-insert-ui.menu.image',
|
||||
icon: 'AdditionAndSubtractionIcon',
|
||||
keywords: ['image', 'picture', 'photo'],
|
||||
};
|
||||
|
||||
export const builtInMenus: DocPopupMenu[] = [
|
||||
{
|
||||
title: 'docs-quick-insert-ui.group.basics',
|
||||
id: QuickInsertMenuGroup.Basic,
|
||||
children: [
|
||||
textMenu,
|
||||
numberedListMenu,
|
||||
bulletedListMenu,
|
||||
dividerMenu,
|
||||
tableMenu,
|
||||
imageMenu,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const builtInMenuCommandIds = new Set([
|
||||
numberedListMenu.id,
|
||||
bulletedListMenu.id,
|
||||
dividerMenu.id,
|
||||
tableMenu.id,
|
||||
imageMenu.id,
|
||||
]);
|
||||
93
packages/docs-quick-insert-ui/src/mobile-plugin.ts
Normal file
93
packages/docs-quick-insert-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverDocsQuickInsertUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
|
||||
import { UniverDocsDrawingMobileUIPlugin } from '@univerjs/docs-drawing-ui';
|
||||
import { UniverDocsMobileUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverDrawingMobileUIPlugin } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, DOCS_QUICK_INSERT_UI_PLUGIN_CONFIG_KEY, DOCS_QUICK_INSERT_UI_PLUGIN_NAME } from './config/config';
|
||||
import { DocQuickInsertTriggerController } from './controllers/doc-quick-insert-trigger.controller';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { DocQuickInsertUIController } from './controllers/ui.controller';
|
||||
import { DocQuickInsertMenuController } from './menu/doc-quick-insert-menu.controller';
|
||||
import { DocQuickInsertPopupService } from './services/doc-quick-insert-popup.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDocsDrawingPlugin,
|
||||
UniverDocsDrawingMobileUIPlugin,
|
||||
UniverDocsMobileUIPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverDrawingMobileUIPlugin,
|
||||
UniverRenderEnginePlugin
|
||||
)
|
||||
export class UniverDocsQuickInsertMobileUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
static override pluginName = DOCS_QUICK_INSERT_UI_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsQuickInsertUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@Inject(IRenderManagerService) private _renderManagerSrv: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(DOCS_QUICK_INSERT_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[MobileComponentsController],
|
||||
[DocQuickInsertUIController],
|
||||
[DocQuickInsertTriggerController],
|
||||
[DocQuickInsertPopupService],
|
||||
];
|
||||
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
|
||||
this._injector.get(MobileComponentsController);
|
||||
this._injector.get(DocQuickInsertUIController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(DocQuickInsertTriggerController);
|
||||
this._injector.get(DocQuickInsertPopupService);
|
||||
|
||||
([
|
||||
[DocQuickInsertMenuController],
|
||||
] as Dependency[]).forEach((m) => {
|
||||
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, m);
|
||||
});
|
||||
}
|
||||
}
|
||||
94
packages/docs-quick-insert-ui/src/plugin.ts
Normal file
94
packages/docs-quick-insert-ui/src/plugin.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverDocsQuickInsertUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
|
||||
import { UniverDocsDrawingUIPlugin } from '@univerjs/docs-drawing-ui';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverDrawingUIPlugin } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, DOCS_QUICK_INSERT_UI_PLUGIN_CONFIG_KEY, DOCS_QUICK_INSERT_UI_PLUGIN_NAME } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { DocQuickInsertTriggerController } from './controllers/doc-quick-insert-trigger.controller';
|
||||
import { DocQuickInsertUIController } from './controllers/ui.controller';
|
||||
import { DocQuickInsertMenuController } from './menu/doc-quick-insert-menu.controller';
|
||||
import { DocQuickInsertPopupService } from './services/doc-quick-insert-popup.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsDrawingPlugin,
|
||||
UniverDocsUIPlugin,
|
||||
UniverDrawingUIPlugin,
|
||||
UniverDocsDrawingUIPlugin
|
||||
)
|
||||
export class UniverDocsQuickInsertUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
static override pluginName = DOCS_QUICK_INSERT_UI_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsQuickInsertUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected _injector: Injector,
|
||||
@Inject(IRenderManagerService) private _renderManagerSrv: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(DOCS_QUICK_INSERT_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[ComponentsController],
|
||||
[DocQuickInsertUIController],
|
||||
[DocQuickInsertTriggerController],
|
||||
[DocQuickInsertPopupService],
|
||||
];
|
||||
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
|
||||
this._injector.get(ComponentsController);
|
||||
this._injector.get(DocQuickInsertUIController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(DocQuickInsertTriggerController);
|
||||
this._injector.get(DocQuickInsertPopupService);
|
||||
|
||||
([
|
||||
[DocQuickInsertMenuController],
|
||||
] as Dependency[]).forEach((m) => {
|
||||
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, m);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IDocumentData } from '@univerjs/core';
|
||||
import { createRequire } from 'node:module';
|
||||
import { DocumentFlavor, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocLayoutExecutorService, DocSelectionManagerService, DocSkeletonManagerService } from '@univerjs/docs';
|
||||
import { DocCanvasPopManagerService, DocEventManagerService, DocLayoutInteractionService } from '@univerjs/docs-ui';
|
||||
import { CanvasColorService, Documents, ICanvasColorService, IRenderManagerService, RenderManagerService, RenderUnit } from '@univerjs/engine-render';
|
||||
import { CanvasPopupService, ICanvasPopupService } from '@univerjs/ui';
|
||||
import { of } from 'rxjs';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { DocQuickInsertPopupService } from '../doc-quick-insert-popup.service';
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('jest', vi);
|
||||
createRequire(import.meta.url)('jest-canvas-mock');
|
||||
});
|
||||
afterAll(() => vi.unstubAllGlobals());
|
||||
|
||||
function createTestBed() {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
|
||||
injector.add([ICanvasPopupService, { useClass: CanvasPopupService }]);
|
||||
injector.add([DocLayoutExecutorService]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocCanvasPopManagerService]);
|
||||
injector.add([DocQuickInsertPopupService]);
|
||||
return { univer, injector };
|
||||
}
|
||||
|
||||
describe('DocQuickInsertPopupService', () => {
|
||||
it('uses the target document layout interaction instead of a global service', () => {
|
||||
const { univer, injector } = createTestBed();
|
||||
try {
|
||||
const model = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, {
|
||||
id: 'quick-insert-doc',
|
||||
body: {
|
||||
dataStream: '/\r\n',
|
||||
paragraphs: [{ startIndex: 1, paragraphId: 'paragraph-1' }],
|
||||
sectionBreaks: [{ startIndex: 2, sectionId: 'body' }],
|
||||
},
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.TRADITIONAL,
|
||||
pageSize: { width: 300, height: 400 },
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
},
|
||||
});
|
||||
const render = injector.get(IRenderManagerService).createRender(model.getUnitId());
|
||||
if (!(render instanceof RenderUnit)) {
|
||||
throw new TypeError('Expected a render unit');
|
||||
}
|
||||
render.deactivate();
|
||||
render.engine.resizeBySize(300, 400);
|
||||
render.addRenderDependencies([[DocSkeletonManagerService], [DocLayoutInteractionService]]);
|
||||
const documents = new Documents('quick-insert-doc', render.with(DocSkeletonManagerService).getSkeleton());
|
||||
render.mainComponent = documents;
|
||||
render.scene.addObject(documents);
|
||||
render.addRenderDependencies([[DocEventManagerService]]);
|
||||
injector.get(DocSelectionManagerService).replaceDocRanges([{
|
||||
startOffset: 1,
|
||||
endOffset: 1,
|
||||
collapsed: true,
|
||||
segmentId: '',
|
||||
}]);
|
||||
const service = injector.get(DocQuickInsertPopupService);
|
||||
const interaction = render.with(DocLayoutInteractionService);
|
||||
const popup = { keyword: '/', menus$: of([]) };
|
||||
const unregister = service.registerPopup(popup);
|
||||
expect(service.resolvePopup('/')).toBe(popup);
|
||||
service.showPopup({ popup, index: 0, unitId: model.getUnitId() });
|
||||
expect(service.editPopup?.unitId).toBe(model.getUnitId());
|
||||
expect(interaction.isActive).toBe(true);
|
||||
service.setIsComposing(true);
|
||||
service.setIsComposing(false);
|
||||
service.closePopup();
|
||||
service.closePopup();
|
||||
expect(interaction.isActive).toBe(false);
|
||||
expect(service.editPopup).toBeNull();
|
||||
service.showPopup({ popup, index: 0, unitId: model.getUnitId() });
|
||||
expect(interaction.isActive).toBe(true);
|
||||
service.dispose();
|
||||
service.dispose();
|
||||
expect(interaction.isActive).toBe(false);
|
||||
expect(injector.get(ICanvasPopupService).popups).toHaveLength(0);
|
||||
unregister();
|
||||
expect(service.resolvePopup('/')).toBeUndefined();
|
||||
} finally {
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not open a popup for a document without a render', () => {
|
||||
const { univer, injector } = createTestBed();
|
||||
try {
|
||||
const service = injector.get(DocQuickInsertPopupService);
|
||||
service.showPopup({ popup: { keyword: '/', menus$: of([]) }, index: 0, unitId: 'missing' });
|
||||
expect(service.editPopup).toBeUndefined();
|
||||
} finally {
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IDisposable, Nullable } from '@univerjs/core';
|
||||
import type { IInsertTextCommandParams } from '@univerjs/docs';
|
||||
import type { Documents, DocumentSkeleton, IBoundRectNoAngle, IDocumentSkeletonGlyph, ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { Disposable, DisposableCollection, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, DocSkeletonManagerService } from '@univerjs/docs';
|
||||
import {
|
||||
DocCanvasPopManagerService,
|
||||
DocEventManagerService,
|
||||
DocLayoutInteractionService,
|
||||
getAnchorBounding,
|
||||
NodePositionConvertToCursor,
|
||||
} from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, map, tap } from 'rxjs';
|
||||
import { DeleteSearchKeyCommand } from '../commands/commands/doc-quick-insert.command';
|
||||
import { KeywordInputPlaceholder } from '../views/KeywordInputPlaceholder';
|
||||
import { QuickInsertPopup } from '../views/QuickInsertPopup';
|
||||
|
||||
export interface IDocPopupGroupItem {
|
||||
id: string;
|
||||
icon?: string;
|
||||
title: string;
|
||||
children?: IDocPopupMenuItem[];
|
||||
}
|
||||
|
||||
export interface IDocPopupMenuItem {
|
||||
id: string;
|
||||
icon?: string;
|
||||
title: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
export type DocPopupMenu = IDocPopupGroupItem | IDocPopupMenuItem;
|
||||
|
||||
export interface IDocPopup {
|
||||
keyword: string;
|
||||
menus$: Observable<DocPopupMenu[]>;
|
||||
Placeholder?: React.ComponentType;
|
||||
preconditions?: (params: IInsertTextCommandParams) => boolean;
|
||||
}
|
||||
|
||||
const noopDisposable = {
|
||||
dispose: () => {},
|
||||
};
|
||||
|
||||
interface IKeywordInputPlaceholderExtraProps extends Record<string, unknown> {
|
||||
fontSize?: number;
|
||||
fontString?: string;
|
||||
fontFamily?: string;
|
||||
fontStyle?: 'normal' | 'italic';
|
||||
fontWeight?: 'normal' | 'bold';
|
||||
ascent?: number;
|
||||
contentHeight?: number;
|
||||
}
|
||||
|
||||
export class DocQuickInsertPopupService extends Disposable {
|
||||
private readonly _popups: Set<IDocPopup> = new Set();
|
||||
get popups() {
|
||||
return Array.from(this._popups);
|
||||
}
|
||||
|
||||
private readonly _editPopup$ = new BehaviorSubject<Nullable<{
|
||||
popup: IDocPopup;
|
||||
anchor: number;
|
||||
disposable: IDisposable;
|
||||
unitId: string;
|
||||
}>>(undefined);
|
||||
|
||||
readonly editPopup$ = this._editPopup$.asObservable();
|
||||
get editPopup() {
|
||||
return this._editPopup$.value;
|
||||
}
|
||||
|
||||
private readonly _isComposing$ = new BehaviorSubject<boolean>(false);
|
||||
readonly isComposing$ = this._isComposing$.asObservable();
|
||||
get isComposing() {
|
||||
return this._isComposing$.value;
|
||||
}
|
||||
|
||||
setIsComposing(isComposing: boolean) {
|
||||
this._isComposing$.next(isComposing);
|
||||
}
|
||||
|
||||
private readonly _inputOffset$ = new BehaviorSubject<{ start: number; end: number }>({ start: 0, end: 0 });
|
||||
readonly inputOffset$ = this._inputOffset$.asObservable();
|
||||
get inputOffset() {
|
||||
return this._inputOffset$.value;
|
||||
}
|
||||
|
||||
setInputOffset(offset: { start: number; end: number }) {
|
||||
this._inputOffset$.next(offset);
|
||||
}
|
||||
|
||||
readonly filterKeyword$: Observable<string>;
|
||||
|
||||
private _menuSelectedCallbacks: Set<(menu: IDocPopupMenuItem) => void> = new Set();
|
||||
|
||||
private _inputPlaceholderRenderRoot: {
|
||||
unmount?: IDisposable;
|
||||
mount: () => void;
|
||||
} | null = null;
|
||||
|
||||
private _getDocEventManagerService(unitId: string) {
|
||||
return this._renderManagerService.getRenderUnitById(unitId)?.with(DocEventManagerService);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(DocCanvasPopManagerService) private readonly _docCanvasPopupManagerService: DocCanvasPopManagerService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(ICommandService) private readonly _commandService: ICommandService,
|
||||
@Inject(IRenderManagerService) private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(DocSelectionManagerService) private readonly _docSelectionManagerService: DocSelectionManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(this._editPopup$);
|
||||
this.disposeWithMe(this._isComposing$);
|
||||
this.disposeWithMe(this._inputOffset$);
|
||||
this.disposeWithMe(this._renderManagerService.disposed$.subscribe((unitId) => {
|
||||
if (this.editPopup?.unitId === unitId) {
|
||||
this.closePopup();
|
||||
}
|
||||
}));
|
||||
|
||||
const getBodySlice = (start: number, end: number) => this._univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC)?.getBody()?.dataStream.slice(start, end);
|
||||
|
||||
let lastFilterKeyword = '';
|
||||
this.filterKeyword$ = this._inputOffset$.pipe(
|
||||
map((offset) => {
|
||||
const slice = getBodySlice(offset.start, offset.end);
|
||||
|
||||
return slice?.slice(1) ?? '';
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
tap((filterKeyword) => {
|
||||
lastFilterKeyword = filterKeyword;
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(combineLatest([
|
||||
this.filterKeyword$.pipe(tap((filterKeyword) => {
|
||||
if (filterKeyword.length > 0) {
|
||||
this._inputPlaceholderRenderRoot?.unmount?.dispose();
|
||||
} else {
|
||||
this._inputPlaceholderRenderRoot?.mount();
|
||||
}
|
||||
})),
|
||||
this.isComposing$.pipe(tap((isComposing) => {
|
||||
if (isComposing) {
|
||||
this._inputPlaceholderRenderRoot?.unmount?.dispose();
|
||||
} else {
|
||||
// If the last filter keyword is empty, after the composition end, mount the input placeholder render root
|
||||
lastFilterKeyword.length <= 0 && this._inputPlaceholderRenderRoot?.mount();
|
||||
}
|
||||
})),
|
||||
this.editPopup$.pipe(tap((popup) => {
|
||||
if (!popup) {
|
||||
this._inputPlaceholderRenderRoot?.unmount?.dispose();
|
||||
this._inputPlaceholderRenderRoot = null;
|
||||
}
|
||||
})),
|
||||
]).subscribe());
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
this.closePopup();
|
||||
this._menuSelectedCallbacks.clear();
|
||||
this._popups.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
resolvePopup(keyword: string) {
|
||||
return Array.from(this._popups).find((popup) => popup.keyword === keyword);
|
||||
}
|
||||
|
||||
registerPopup(popup: IDocPopup) {
|
||||
this._popups.add(popup);
|
||||
|
||||
return () => {
|
||||
this._popups.delete(popup);
|
||||
};
|
||||
}
|
||||
|
||||
private _createInputPlaceholderRenderRoot(mount: () => IDisposable) {
|
||||
const renderRoot: {
|
||||
isMounted: boolean;
|
||||
unmount?: IDisposable;
|
||||
mount: () => void;
|
||||
} = {
|
||||
isMounted: false,
|
||||
mount() {
|
||||
// Prevent duplicate mounting
|
||||
if (this.isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isMounted = true;
|
||||
|
||||
const unmount = mount();
|
||||
|
||||
this.unmount = {
|
||||
dispose: () => {
|
||||
unmount.dispose();
|
||||
this.isMounted = false;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return renderRoot;
|
||||
}
|
||||
|
||||
private _getParagraphBound(unitId: string, index: number) {
|
||||
const currentDoc = this._univerInstanceService.getUnit<DocumentDataModel>(unitId);
|
||||
const paragraph = currentDoc?.getBody()?.paragraphs?.find((p) => p.startIndex > index);
|
||||
if (!paragraph) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const docEventManagerService = this._getDocEventManagerService(unitId);
|
||||
return docEventManagerService?.findParagraphBoundByIndex(paragraph.startIndex) ?? null;
|
||||
}
|
||||
|
||||
private _getKeywordPlaceholderAnchorRect(
|
||||
document: Documents,
|
||||
skeleton: DocumentSkeleton,
|
||||
activeRange: ITextRangeWithStyle,
|
||||
fallbackRect: IBoundRectNoAngle
|
||||
): IBoundRectNoAngle {
|
||||
const startPosition = skeleton.findNodePositionByCharIndex(activeRange.startOffset, true, activeRange.segmentId, activeRange.segmentPage);
|
||||
if (!startPosition) {
|
||||
return fallbackRect;
|
||||
}
|
||||
|
||||
const documentOffsetConfig = document.getOffsetConfig();
|
||||
const convertor = new NodePositionConvertToCursor(documentOffsetConfig, skeleton);
|
||||
const { contentBoxPointGroup } = convertor.getRangePointData(startPosition, startPosition);
|
||||
|
||||
if (contentBoxPointGroup.length === 0) {
|
||||
return fallbackRect;
|
||||
}
|
||||
|
||||
const anchor = getAnchorBounding(contentBoxPointGroup);
|
||||
const left = anchor.left + documentOffsetConfig.docsLeft;
|
||||
const top = anchor.top + documentOffsetConfig.docsTop;
|
||||
|
||||
return {
|
||||
left,
|
||||
right: left,
|
||||
top,
|
||||
bottom: top + anchor.height,
|
||||
};
|
||||
}
|
||||
|
||||
private _getKeywordPlaceholderExtraProps(curGlyph: IDocumentSkeletonGlyph): IKeywordInputPlaceholderExtraProps {
|
||||
return {
|
||||
fontSize: curGlyph.ts?.fs,
|
||||
fontString: curGlyph.fontStyle?.fontString,
|
||||
fontFamily: curGlyph.fontStyle?.fontFamily ?? curGlyph.ts?.ff ?? undefined,
|
||||
fontStyle: curGlyph.ts?.it ? 'italic' : 'normal',
|
||||
fontWeight: curGlyph.ts?.bl ? 'bold' : 'normal',
|
||||
ascent: curGlyph.bBox?.ba,
|
||||
contentHeight: ((curGlyph.bBox?.ba ?? 0) + (curGlyph.bBox?.bd ?? 0)) || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _mountInputPlaceholder(unitId: string, fallbackRect: IBoundRectNoAngle): IDisposable {
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const docSkeletonManagerService = currentRender?.with(DocSkeletonManagerService);
|
||||
const activeRange = this._docSelectionManagerService.getActiveTextRange();
|
||||
if (!currentRender || !docSkeletonManagerService || !activeRange) {
|
||||
return noopDisposable;
|
||||
}
|
||||
|
||||
const skeleton = docSkeletonManagerService.getSkeleton();
|
||||
const curGlyph = skeleton.findNodeByCharIndex(activeRange.startOffset, activeRange.segmentId, activeRange.segmentPage);
|
||||
const isEmptyLine = curGlyph?.content === '\r';
|
||||
if (!isEmptyLine || !curGlyph) {
|
||||
return noopDisposable;
|
||||
}
|
||||
|
||||
const document = currentRender.mainComponent as Documents;
|
||||
const placeholderAnchorRect = this._getKeywordPlaceholderAnchorRect(document, skeleton, activeRange, fallbackRect);
|
||||
const extraProps = this._getKeywordPlaceholderExtraProps(curGlyph);
|
||||
|
||||
const disposable = this._docCanvasPopupManagerService.attachPopupToRect(
|
||||
placeholderAnchorRect,
|
||||
{
|
||||
componentKey: KeywordInputPlaceholder.componentKey,
|
||||
extraProps,
|
||||
onClickOutside: () => {
|
||||
disposable.dispose();
|
||||
},
|
||||
direction: 'horizontal',
|
||||
},
|
||||
unitId
|
||||
);
|
||||
|
||||
return disposable;
|
||||
}
|
||||
|
||||
showPopup(options: { popup: IDocPopup; index: number; unitId: string }) {
|
||||
const { popup, index, unitId } = options;
|
||||
this.closePopup();
|
||||
const render = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (!render) {
|
||||
return;
|
||||
}
|
||||
const paragraphBound = this._getParagraphBound(unitId, index);
|
||||
if (!paragraphBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputPlaceholderRenderRoot = this._createInputPlaceholderRenderRoot(() => this._mountInputPlaceholder(unitId, paragraphBound.firstLine));
|
||||
this._inputPlaceholderRenderRoot.mount();
|
||||
|
||||
const layoutInteraction = render.with(DocLayoutInteractionService).beginInteraction();
|
||||
const popupDisposable = this._docCanvasPopupManagerService.attachPopupToRect(
|
||||
paragraphBound.firstLine,
|
||||
{
|
||||
componentKey: QuickInsertPopup.componentKey,
|
||||
onClickOutside: () => {
|
||||
this.closePopup();
|
||||
},
|
||||
direction: 'bottom',
|
||||
},
|
||||
unitId
|
||||
);
|
||||
const disposable = new DisposableCollection();
|
||||
disposable.add(popupDisposable);
|
||||
disposable.add(layoutInteraction);
|
||||
|
||||
this._editPopup$.next({ disposable, popup, anchor: index, unitId });
|
||||
}
|
||||
|
||||
closePopup() {
|
||||
if (this.editPopup) {
|
||||
this.editPopup.disposable.dispose();
|
||||
this._editPopup$.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
onMenuSelected(callback: (menu: IDocPopupMenuItem) => void) {
|
||||
this._menuSelectedCallbacks.add(callback);
|
||||
|
||||
return () => {
|
||||
this._menuSelectedCallbacks.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
emitMenuSelected(menu: IDocPopupMenuItem) {
|
||||
const { start, end } = this.inputOffset;
|
||||
this._commandService.syncExecuteCommand(DeleteSearchKeyCommand.id, {
|
||||
start,
|
||||
end,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this._menuSelectedCallbacks.forEach((callback) => callback(menu));
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IPopup } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { ptToPixel } from '@univerjs/engine-render';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const KeywordInputPlaceholderComponentKey = 'docs.quick.insert.keyword-input-placeholder';
|
||||
|
||||
const DEFAULT_FONT_SIZE = 11;
|
||||
|
||||
interface IKeywordInputPlaceholderExtraProps {
|
||||
fontSize?: number;
|
||||
fontString?: string;
|
||||
fontFamily?: string;
|
||||
fontStyle?: 'normal' | 'italic';
|
||||
fontWeight?: 'normal' | 'bold';
|
||||
ascent?: number;
|
||||
contentHeight?: number;
|
||||
}
|
||||
|
||||
function measureTextWidth(text: string, font: string) {
|
||||
if (typeof document === 'undefined') {
|
||||
return text.length * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
return text.length * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
context.font = font;
|
||||
|
||||
return Math.ceil(context.measureText(text).width);
|
||||
}
|
||||
|
||||
export const KeywordInputPlaceholder = ({ popup }: { popup: IPopup<IKeywordInputPlaceholderExtraProps> }) => {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const placeholder = localeService.t<LocaleKey>('docs-quick-insert-ui.keywordInputPlaceholder');
|
||||
const fontSize = popup.extraProps?.fontSize ?? DEFAULT_FONT_SIZE;
|
||||
const fontSizePx = ptToPixel(fontSize);
|
||||
const fontString = popup.extraProps?.fontString ?? `${fontSizePx}px sans-serif`;
|
||||
const ascent = popup.extraProps?.ascent ?? fontSizePx;
|
||||
const contentHeight = Math.max(popup.extraProps?.contentHeight ?? fontSizePx, fontSizePx);
|
||||
const textWidth = useMemo(() => measureTextWidth(placeholder, fontString), [fontString, placeholder]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-select-none univer-font-normal univer-text-gray-500 univer-transition-colors
|
||||
dark:!univer-text-gray-400
|
||||
`}
|
||||
>
|
||||
<svg
|
||||
width={textWidth}
|
||||
height={contentHeight}
|
||||
viewBox={`0 0 ${textWidth} ${contentHeight}`}
|
||||
style={{ overflow: 'visible', display: 'block' }}
|
||||
>
|
||||
<text
|
||||
x={0}
|
||||
y={ascent}
|
||||
fill="currentColor"
|
||||
style={{
|
||||
font: fontString,
|
||||
fontFamily: popup.extraProps?.fontFamily,
|
||||
fontStyle: popup.extraProps?.fontStyle,
|
||||
fontWeight: popup.extraProps?.fontWeight,
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
KeywordInputPlaceholder.componentKey = KeywordInputPlaceholderComponentKey;
|
||||
104
packages/docs-quick-insert-ui/src/views/QuickInsertButton.tsx
Normal file
104
packages/docs-quick-insert-ui/src/views/QuickInsertButton.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel } from '@univerjs/core';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import type { IDocPopup } from '../services/doc-quick-insert-popup.service';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { borderClassName, clsx } from '@univerjs/design';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { IncreaseIcon } from '@univerjs/icons';
|
||||
import { ILayoutService, useDependency, useEvent, useObservable } from '@univerjs/ui';
|
||||
import { useMemo } from 'react';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
import { DocQuickInsertMenuController } from '../menu/doc-quick-insert-menu.controller';
|
||||
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
|
||||
|
||||
export const QuickInsertButtonComponentKey = 'doc.quick-insert.button';
|
||||
|
||||
export const QuickInsertButton = ({ className = '' }: {
|
||||
className?: string;
|
||||
}) => {
|
||||
const docQuickInsertPopupService = useDependency(DocQuickInsertPopupService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const currentDoc = useObservable(useMemo(() => univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC), [univerInstanceService]));
|
||||
const currentUnit = currentDoc && renderManagerService.getRenderUnitById(currentDoc.getUnitId());
|
||||
const docQuickInsertMenuController = currentUnit?.with(DocQuickInsertMenuController);
|
||||
const layoutService = useDependency(ILayoutService);
|
||||
const docSelectionManagerService = useDependency(DocSelectionManagerService);
|
||||
const editPopup = useObservable(docQuickInsertPopupService.editPopup$);
|
||||
|
||||
const onClick: MouseEventHandler<HTMLDivElement> = useEvent(() => {
|
||||
const p = docQuickInsertMenuController?.popup;
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allPopups = docQuickInsertPopupService.popups;
|
||||
// combine all popups into one
|
||||
const popup: IDocPopup = {
|
||||
keyword: '',
|
||||
menus$: combineLatest(allPopups.map((p) => p.menus$))
|
||||
.pipe(
|
||||
map((menusCollection) => menusCollection.flat())
|
||||
),
|
||||
};
|
||||
|
||||
docSelectionManagerService.replaceDocRanges([{
|
||||
startOffset: p.startIndex,
|
||||
endOffset: p.startIndex,
|
||||
}]);
|
||||
docQuickInsertPopupService.setInputOffset({ start: p.startIndex - 1, end: p.startIndex - 1 });
|
||||
docQuickInsertPopupService.showPopup({
|
||||
popup,
|
||||
index: p.startIndex - 1,
|
||||
unitId: currentDoc?.getUnitId() ?? '',
|
||||
});
|
||||
setTimeout(() => {
|
||||
// keep the cursor in doc
|
||||
layoutService.focus();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-mr-1 univer-flex univer-cursor-pointer univer-items-center univer-gap-2.5 univer-rounded-full
|
||||
univer-p-1.5 univer-shadow-sm
|
||||
hover:univer-bg-gray-100
|
||||
dark:!univer-text-gray-200
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`, borderClassName, {
|
||||
'univer-bg-gray-100 dark:!univer-bg-gray-700': editPopup,
|
||||
'univer-bg-gray-0 dark:!univer-bg-gray-900': !editPopup,
|
||||
}, className)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IncreaseIcon
|
||||
className={`
|
||||
univer-text-gray-800
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
QuickInsertButton.componentKey = QuickInsertButtonComponentKey;
|
||||
173
packages/docs-quick-insert-ui/src/views/QuickInsertMenu.tsx
Normal file
173
packages/docs-quick-insert-ui/src/views/QuickInsertMenu.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocPopupMenu, IDocPopupMenuItem } from '../services/doc-quick-insert-popup.service';
|
||||
import { borderBottomClassName, borderClassName, clsx, scrollbarClassName, Tooltip } from '@univerjs/design';
|
||||
import { IconManager, useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
interface IQuickInsertMenuProps {
|
||||
menus: DocPopupMenu[];
|
||||
focusedMenuIndex: number;
|
||||
onFocusedMenuIndexChange: (index: number) => void;
|
||||
onFocusedMenuChange: (menu: IDocPopupMenuItem | null) => void;
|
||||
onSelect: (menu: IDocPopupMenuItem) => void;
|
||||
}
|
||||
|
||||
function isMenuGroup(menu: DocPopupMenu): menu is DocPopupMenu & { children: IDocPopupMenuItem[] } {
|
||||
return 'children' in menu;
|
||||
}
|
||||
|
||||
function flattenMenuItems(menus: DocPopupMenu[]): IDocPopupMenuItem[] {
|
||||
return menus.flatMap((menu) => {
|
||||
if (isMenuGroup(menu)) {
|
||||
return flattenMenuItems(menu.children);
|
||||
}
|
||||
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
|
||||
export function getQuickInsertMenuLeafCount(menus: DocPopupMenu[]) {
|
||||
return flattenMenuItems(menus).length;
|
||||
}
|
||||
|
||||
export function QuickInsertMenu(props: IQuickInsertMenuProps) {
|
||||
const {
|
||||
menus,
|
||||
focusedMenuIndex,
|
||||
onFocusedMenuIndexChange,
|
||||
onFocusedMenuChange,
|
||||
onSelect,
|
||||
} = props;
|
||||
|
||||
const iconManager = useDependency(IconManager);
|
||||
const flatMenus = useMemo(() => flattenMenuItems(menus), [menus]);
|
||||
const menuNodeMapRef = useRef(new Map<string, HTMLElement>());
|
||||
|
||||
useEffect(() => {
|
||||
const focusedMenu = Number.isNaN(focusedMenuIndex)
|
||||
? null
|
||||
: flatMenus[focusedMenuIndex] ?? null;
|
||||
|
||||
onFocusedMenuChange(focusedMenu);
|
||||
|
||||
if (!focusedMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
menuNodeMapRef.current.get(focusedMenu.id)?.scrollIntoView({
|
||||
block: 'nearest',
|
||||
});
|
||||
}, [flatMenus, focusedMenuIndex, onFocusedMenuChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const menuNodeMap = menuNodeMapRef.current;
|
||||
|
||||
return () => {
|
||||
menuNodeMap.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const itemIndexRef = useRef(0);
|
||||
itemIndexRef.current = 0;
|
||||
|
||||
function renderMenus(currentMenus: DocPopupMenu[]) {
|
||||
return currentMenus.map((menu, index) => {
|
||||
const iconKey = menu.icon;
|
||||
const Icon = iconKey ? iconManager.get(iconKey) : null;
|
||||
|
||||
if (isMenuGroup(menu)) {
|
||||
return (
|
||||
<div
|
||||
key={menu.id}
|
||||
className={clsx('univer-grid univer-gap-1 univer-py-1', index !== currentMenus.length - 1 && borderBottomClassName)}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
univer-box-border univer-inline-flex univer-items-center univer-gap-2 univer-px-2
|
||||
univer-text-xs univer-font-semibold univer-text-gray-600
|
||||
dark:!univer-text-gray-300
|
||||
`}
|
||||
>
|
||||
{Icon && <span className="univer-inline-flex univer-text-base"><Icon /></span>}
|
||||
<span>{menu.title}</span>
|
||||
</div>
|
||||
<div className="univer-grid univer-gap-1">
|
||||
{renderMenus(menu.children)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const currentMenuIndex = itemIndexRef.current;
|
||||
const isFocused = focusedMenuIndex === currentMenuIndex;
|
||||
itemIndexRef.current += 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={menu.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
menuNodeMapRef.current.set(menu.id, node);
|
||||
return;
|
||||
}
|
||||
|
||||
menuNodeMapRef.current.delete(menu.id);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
className={clsx(`
|
||||
univer-relative univer-box-border univer-flex univer-w-full univer-cursor-pointer
|
||||
univer-items-center univer-justify-between univer-gap-3 univer-rounded-md univer-border-none
|
||||
univer-bg-transparent univer-px-2 univer-text-left univer-text-sm univer-text-gray-900
|
||||
univer-outline-none
|
||||
dark:!univer-text-gray-0
|
||||
`, {
|
||||
'univer-min-h-8 hover:univer-bg-gray-50 dark:hover:!univer-bg-gray-600': true,
|
||||
'hover:univer-bg-transparent': !isFocused,
|
||||
'univer-bg-gray-50 dark:!univer-bg-gray-600': isFocused,
|
||||
})}
|
||||
onMouseEnter={() => onFocusedMenuIndexChange(currentMenuIndex)}
|
||||
onMouseLeave={() => onFocusedMenuIndexChange(Number.NaN)}
|
||||
onClick={() => onSelect(menu)}
|
||||
>
|
||||
<div className="univer-inline-flex univer-w-full univer-items-center univer-gap-2">
|
||||
{Icon && <span className="univer-inline-flex univer-text-base"><Icon /></span>}
|
||||
<Tooltip showIfEllipsis title={menu.title} placement="right">
|
||||
<span className="univer-truncate">{menu.title}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-box-border univer-grid univer-gap-1 univer-overflow-y-auto univer-overflow-x-hidden
|
||||
univer-overscroll-contain univer-bg-gray-0 univer-px-2 univer-text-gray-900 univer-shadow-md
|
||||
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
|
||||
`, borderClassName, scrollbarClassName, {
|
||||
'univer-max-h-[360px] univer-rounded-md univer-py-1 univer-text-sm': true,
|
||||
})}
|
||||
onWheel={(event) => event.stopPropagation()}
|
||||
>
|
||||
{renderMenus(menus)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
export const QuickInsertPlaceholderComponentKey = 'docs.quick.insert.placeholder';
|
||||
|
||||
export const QuickInsertPlaceholder = () => {
|
||||
const localeService = useDependency(LocaleService);
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-h-full univer-items-center univer-justify-center univer-rounded-lg univer-bg-gray-0
|
||||
univer-px-12 univer-py-6 univer-text-gray-400 univer-shadow-lg
|
||||
`}
|
||||
>
|
||||
<span>{localeService.t<LocaleKey>('docs-quick-insert-ui.placeholder')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
QuickInsertPlaceholder.componentKey = QuickInsertPlaceholderComponentKey;
|
||||
168
packages/docs-quick-insert-ui/src/views/QuickInsertPopup.tsx
Normal file
168
packages/docs-quick-insert-ui/src/views/QuickInsertPopup.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDocPopupMenuItem } from '../services/doc-quick-insert-popup.service';
|
||||
import {
|
||||
CommandType,
|
||||
Direction,
|
||||
DisposableCollection,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { IShortcutService, KeyCode, useDependency } from '@univerjs/ui';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getQuickInsertMenuLeafCount, QuickInsertMenu } from './QuickInsertMenu';
|
||||
|
||||
import { useQuickInsertPopup } from './use-quick-insert-popup';
|
||||
|
||||
const interceptKeys = [KeyCode.ARROW_UP, KeyCode.ARROW_DOWN, KeyCode.ENTER];
|
||||
|
||||
export const QuickInsertPopup = () => {
|
||||
const shortcutService = useDependency(IShortcutService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const id = useMemo(() => generateRandomId(), []);
|
||||
const { filteredMenus, handleMenuSelect, Placeholder } = useQuickInsertPopup();
|
||||
const [focusedMenuIndex, setFocusedMenuIndex] = useState(0);
|
||||
const focusedMenuRef = useRef<IDocPopupMenuItem | null>(null);
|
||||
const filteredMenuCount = getQuickInsertMenuLeafCount(filteredMenus);
|
||||
const filteredMenuCountRef = useRef(filteredMenuCount);
|
||||
filteredMenuCountRef.current = filteredMenuCount;
|
||||
const handleFocusedMenuChange = useCallback((menu: IDocPopupMenuItem | null) => {
|
||||
focusedMenuRef.current = menu;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
/** Use up or down to navigate the focused menu instead of moving the cursor in documents. */
|
||||
const disposableCollection = new DisposableCollection();
|
||||
|
||||
const shortcutItems = shortcutService.getAllShortcuts();
|
||||
|
||||
const interceptedShortcutItems = shortcutItems.filter((item) => item.binding && interceptKeys.includes(item.binding));
|
||||
// disable the shortcut items of moving the cursor in documents
|
||||
interceptedShortcutItems.forEach((item) => {
|
||||
const rawPreconditions = item.preconditions;
|
||||
|
||||
item.preconditions = () => false;
|
||||
|
||||
disposableCollection.add(toDisposable(() => {
|
||||
item.preconditions = rawPreconditions;
|
||||
}));
|
||||
});
|
||||
|
||||
const enterCommand = {
|
||||
id: `quick.insert.popup.enter.${id}`,
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => {
|
||||
const menu = focusedMenuRef.current;
|
||||
if (menu) {
|
||||
handleMenuSelect(menu);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const moveCursorUpCommand = {
|
||||
id: `quick.insert.popup.move.cursor.up.${id}`,
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => {
|
||||
setFocusedMenuIndex((index) => {
|
||||
if (filteredMenuCountRef.current <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nextIndex = (index - 1);
|
||||
|
||||
return nextIndex >= 0 ? nextIndex : filteredMenuCountRef.current - 1;
|
||||
});
|
||||
},
|
||||
};
|
||||
const moveCursorDownCommand = {
|
||||
id: `quick.insert.popup.move.cursor.down.${id}`,
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => {
|
||||
setFocusedMenuIndex((index) => {
|
||||
if (filteredMenuCountRef.current <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nextIndex = (index + 1);
|
||||
|
||||
return nextIndex <= (filteredMenuCountRef.current - 1) ? nextIndex : 0;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// register the commands of moving the focused menu up and down
|
||||
disposableCollection.add(commandService.registerCommand(moveCursorUpCommand));
|
||||
disposableCollection.add(commandService.registerCommand(moveCursorDownCommand));
|
||||
disposableCollection.add(commandService.registerCommand(enterCommand));
|
||||
disposableCollection.add(shortcutService.registerShortcut({
|
||||
priority: 1000,
|
||||
id: moveCursorUpCommand.id,
|
||||
binding: KeyCode.ARROW_UP,
|
||||
preconditions: () => true,
|
||||
staticParameters: {
|
||||
direction: Direction.UP,
|
||||
},
|
||||
}));
|
||||
|
||||
disposableCollection.add(shortcutService.registerShortcut({
|
||||
priority: 1000,
|
||||
id: moveCursorDownCommand.id,
|
||||
binding: KeyCode.ARROW_DOWN,
|
||||
preconditions: () => true,
|
||||
staticParameters: {
|
||||
direction: Direction.DOWN,
|
||||
},
|
||||
}));
|
||||
|
||||
disposableCollection.add(shortcutService.registerShortcut({
|
||||
priority: 1000,
|
||||
id: enterCommand.id,
|
||||
binding: KeyCode.ENTER,
|
||||
preconditions: () => true,
|
||||
}));
|
||||
|
||||
return () => {
|
||||
disposableCollection.dispose();
|
||||
};
|
||||
}, [commandService, id, shortcutService]);
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedMenuIndex(0);
|
||||
}, [filteredMenus]);
|
||||
|
||||
const hasMenus = filteredMenus.length > 0;
|
||||
|
||||
return (
|
||||
<div className="univer-mt-2">
|
||||
{hasMenus
|
||||
? (
|
||||
<QuickInsertMenu
|
||||
menus={filteredMenus}
|
||||
focusedMenuIndex={focusedMenuIndex}
|
||||
onFocusedMenuIndexChange={setFocusedMenuIndex}
|
||||
onFocusedMenuChange={handleFocusedMenuChange}
|
||||
onSelect={handleMenuSelect}
|
||||
/>
|
||||
)
|
||||
: Placeholder && <Placeholder />}
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
QuickInsertPopup.componentKey = 'docs.quick.insert.popup';
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Root } from 'react-dom/client';
|
||||
import type { IDocPopupMenuItem } from '../../services/doc-quick-insert-popup.service';
|
||||
import { DesktopLogService, ILogService, Injector } from '@univerjs/core';
|
||||
import { IconManager, RediContext } from '@univerjs/ui';
|
||||
import { act, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { QuickInsertMenu } from '../QuickInsertMenu';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const insertTableMenu: IDocPopupMenuItem = {
|
||||
id: 'insert-table',
|
||||
title: 'Table',
|
||||
};
|
||||
|
||||
const insertImageMenu: IDocPopupMenuItem = {
|
||||
id: 'insert-image',
|
||||
title: 'Image',
|
||||
};
|
||||
|
||||
const insertEquationMenu: IDocPopupMenuItem = {
|
||||
id: 'insert-equation',
|
||||
title: 'Equation',
|
||||
};
|
||||
|
||||
const nestedMenus = [
|
||||
{
|
||||
id: 'insert-group',
|
||||
title: 'Insert',
|
||||
children: [
|
||||
insertTableMenu,
|
||||
insertImageMenu,
|
||||
],
|
||||
},
|
||||
insertEquationMenu,
|
||||
];
|
||||
|
||||
class TestState {
|
||||
static focusedMenus: Array<IDocPopupMenuItem | null> = [];
|
||||
static focusedIndexes: number[] = [];
|
||||
static selectedMenus: IDocPopupMenuItem[] = [];
|
||||
|
||||
static reset(): void {
|
||||
this.focusedMenus = [];
|
||||
this.focusedIndexes = [];
|
||||
this.selectedMenus = [];
|
||||
}
|
||||
}
|
||||
|
||||
function createInjector() {
|
||||
const injector = new Injector();
|
||||
injector.add([ILogService, { useClass: DesktopLogService }]);
|
||||
injector.add([IconManager, { useClass: IconManager }]);
|
||||
|
||||
return injector;
|
||||
}
|
||||
|
||||
function QuickInsertMenuHarness() {
|
||||
const [focusedMenuIndex, setFocusedMenuIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<QuickInsertMenu
|
||||
menus={nestedMenus}
|
||||
focusedMenuIndex={focusedMenuIndex}
|
||||
onFocusedMenuIndexChange={(index) => {
|
||||
TestState.focusedIndexes.push(index);
|
||||
setFocusedMenuIndex(index);
|
||||
}}
|
||||
onFocusedMenuChange={(menu) => TestState.focusedMenus.push(menu)}
|
||||
onSelect={(menu) => TestState.selectedMenus.push(menu)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
describe('QuickInsertMenu', () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let scrollIntoViewDescriptor: PropertyDescriptor | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
TestState.reset();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
scrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollIntoView');
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
if (scrollIntoViewDescriptor) {
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', scrollIntoViewDescriptor);
|
||||
} else {
|
||||
delete (Element.prototype as { scrollIntoView?: unknown }).scrollIntoView;
|
||||
}
|
||||
});
|
||||
|
||||
function renderMenu() {
|
||||
const injector = createInjector();
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<QuickInsertMenuHarness />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function findMenuItem(title: string) {
|
||||
return Array.from(container.querySelectorAll('[role="button"]'))
|
||||
.find((node) => node.textContent === title) as HTMLElement | undefined;
|
||||
}
|
||||
|
||||
it('maps nested menu leaves to keyboard focus order and selects the hovered business item', () => {
|
||||
renderMenu();
|
||||
|
||||
expect(TestState.focusedMenus.at(-1)).toBe(insertTableMenu);
|
||||
|
||||
const imageMenuNode = findMenuItem('Image');
|
||||
expect(imageMenuNode).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
imageMenuNode!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(TestState.focusedIndexes).toEqual([1]);
|
||||
expect(TestState.focusedMenus.at(-1)).toBe(insertImageMenu);
|
||||
|
||||
act(() => {
|
||||
imageMenuNode!.click();
|
||||
});
|
||||
|
||||
expect(TestState.selectedMenus).toEqual([insertImageMenu]);
|
||||
});
|
||||
|
||||
it('releases the focused menu when the pointer leaves a business item', () => {
|
||||
renderMenu();
|
||||
|
||||
const tableMenuNode = findMenuItem('Table');
|
||||
expect(tableMenuNode).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
tableMenuNode!.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(Number.isNaN(TestState.focusedIndexes.at(-1))).toBe(true);
|
||||
expect(TestState.focusedMenus.at(-1)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IDocumentData } from '@univerjs/core';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import type { DocPopupMenu } from '../../services/doc-quick-insert-popup.service';
|
||||
import { createRequire } from 'node:module';
|
||||
import { DocumentFlavor, ICommandService, LocaleService, LocaleType, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocLayoutExecutorService, DocSelectionManagerService, DocSkeletonManagerService, DocStateEmitService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { CutContentCommand, DocCanvasPopManagerService, DocEventManagerService, DocLayoutInteractionService } from '@univerjs/docs-ui';
|
||||
import { CanvasColorService, Documents, ICanvasColorService, IRenderManagerService, RenderManagerService, RenderUnit } from '@univerjs/engine-render';
|
||||
import {
|
||||
CanvasPopupService,
|
||||
ComponentManager,
|
||||
ICanvasPopupService,
|
||||
IconManager,
|
||||
IPlatformService,
|
||||
IShortcutService,
|
||||
IUIRuntimeScopeService,
|
||||
KeyCode,
|
||||
PlatformService,
|
||||
RediContext,
|
||||
ShortcutService,
|
||||
UIRuntimeScopeService,
|
||||
} from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DeleteSearchKeyCommand } from '../../commands/commands/doc-quick-insert.command';
|
||||
import { CloseQuickInsertPopupOperation } from '../../commands/operations/quick-insert-popup.operation';
|
||||
import { DocQuickInsertPopupService } from '../../services/doc-quick-insert-popup.service';
|
||||
import { QuickInsertPlaceholder } from '../QuickInsertPlaceholder';
|
||||
import { QuickInsertPopup } from '../QuickInsertPopup';
|
||||
|
||||
const testUnivers: Univer[] = [];
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
vi.stubGlobal('jest', vi);
|
||||
createRequire(import.meta.url)('jest-canvas-mock');
|
||||
});
|
||||
afterAll(() => vi.unstubAllGlobals());
|
||||
|
||||
const TEST_DOC_UNIT_ID = 'doc-quick-insert-test';
|
||||
|
||||
function createQuickInsertPopupTestBed(options?: {
|
||||
dataStream?: string;
|
||||
inputOffset?: { start: number; end: number };
|
||||
}) {
|
||||
const univer = new Univer();
|
||||
testUnivers.push(univer);
|
||||
const injector = univer.__getInjector();
|
||||
let cutContentParams: unknown;
|
||||
let selectedMenu: unknown;
|
||||
|
||||
injector.add([IPlatformService, { useClass: PlatformService }]);
|
||||
injector.add([IUIRuntimeScopeService, { useClass: UIRuntimeScopeService }]);
|
||||
injector.add([IShortcutService, { useClass: ShortcutService }]);
|
||||
injector.add([ComponentManager, { useClass: ComponentManager }]);
|
||||
injector.add([IconManager, { useClass: IconManager }]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
|
||||
injector.add([ICanvasPopupService, { useClass: CanvasPopupService }]);
|
||||
injector.add([DocLayoutExecutorService]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocStateEmitService]);
|
||||
injector.add([DocCanvasPopManagerService]);
|
||||
injector.add([DocQuickInsertPopupService]);
|
||||
const dataStream = options?.dataStream ?? '/ta\r\n';
|
||||
const model = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, {
|
||||
id: TEST_DOC_UNIT_ID,
|
||||
body: {
|
||||
dataStream,
|
||||
paragraphs: [{ startIndex: dataStream.length - 2, paragraphId: 'popup-paragraph' }],
|
||||
sectionBreaks: [{ startIndex: dataStream.length - 1, sectionId: 'body' }],
|
||||
},
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.TRADITIONAL,
|
||||
pageSize: { width: 300, height: 400 },
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
},
|
||||
});
|
||||
const render = injector.get(IRenderManagerService).createRender(model.getUnitId());
|
||||
if (!(render instanceof RenderUnit)) {
|
||||
throw new TypeError('Expected a render unit');
|
||||
}
|
||||
render.deactivate();
|
||||
render.engine.resizeBySize(300, 400);
|
||||
render.addRenderDependencies([[DocSkeletonManagerService], [DocLayoutInteractionService]]);
|
||||
const documents = new Documents(TEST_DOC_UNIT_ID, render.with(DocSkeletonManagerService).getSkeleton());
|
||||
render.mainComponent = documents;
|
||||
render.scene.addObject(documents);
|
||||
render.addRenderDependencies([[DocEventManagerService]]);
|
||||
injector.get(DocSelectionManagerService).replaceDocRanges([{
|
||||
startOffset: dataStream.length - 2,
|
||||
endOffset: dataStream.length - 2,
|
||||
collapsed: true,
|
||||
segmentId: '',
|
||||
}]);
|
||||
|
||||
const localeService = injector.get(LocaleService);
|
||||
localeService.load({
|
||||
[LocaleType.ZH_CN]: {
|
||||
quick: {
|
||||
insert: {
|
||||
group: 'Insert',
|
||||
table: 'Table',
|
||||
image: 'Image',
|
||||
},
|
||||
},
|
||||
'docs-quick-insert-ui': {
|
||||
placeholder: 'No result',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
localeService.setLocale(LocaleType.ZH_CN);
|
||||
const componentManager = injector.get(ComponentManager);
|
||||
componentManager.register(QuickInsertPlaceholder.componentKey, QuickInsertPlaceholder);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(CloseQuickInsertPopupOperation);
|
||||
commandService.registerCommand(DeleteSearchKeyCommand);
|
||||
commandService.registerCommand(CutContentCommand);
|
||||
commandService.registerCommand(RichTextEditingMutation);
|
||||
commandService.onCommandExecuted((command) => {
|
||||
if (command.id === CutContentCommand.id) {
|
||||
cutContentParams = command.params;
|
||||
}
|
||||
});
|
||||
|
||||
const menus$ = new BehaviorSubject<DocPopupMenu[]>([
|
||||
{
|
||||
id: 'insert-group',
|
||||
title: 'quick.insert.group',
|
||||
children: [
|
||||
{
|
||||
id: 'insert-table',
|
||||
title: 'quick.insert.table',
|
||||
keywords: ['table', 'grid'],
|
||||
},
|
||||
{
|
||||
id: 'insert-image',
|
||||
title: 'quick.insert.image',
|
||||
keywords: ['image', 'picture'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const popupService = injector.get(DocQuickInsertPopupService);
|
||||
popupService.onMenuSelected((menu) => {
|
||||
selectedMenu = menu;
|
||||
});
|
||||
popupService.showPopup({
|
||||
popup: {
|
||||
keyword: '/',
|
||||
menus$,
|
||||
},
|
||||
index: 0,
|
||||
unitId: TEST_DOC_UNIT_ID,
|
||||
});
|
||||
popupService.setInputOffset(options?.inputOffset ?? { start: 0, end: 3 });
|
||||
|
||||
return {
|
||||
getCutContentParams: () => cutContentParams,
|
||||
getSelectedMenu: () => selectedMenu,
|
||||
injector,
|
||||
popupService,
|
||||
popupManagerService: injector.get(ICanvasPopupService),
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
describe('QuickInsertPopup', () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let scrollIntoViewDescriptor: PropertyDescriptor | undefined;
|
||||
let requestIdleCallbackDescriptor: PropertyDescriptor | undefined;
|
||||
let cancelIdleCallbackDescriptor: PropertyDescriptor | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
scrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollIntoView');
|
||||
requestIdleCallbackDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'requestIdleCallback');
|
||||
cancelIdleCallbackDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'cancelIdleCallback');
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: () => {},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'requestIdleCallback', {
|
||||
configurable: true,
|
||||
value: (callback: IdleRequestCallback) => {
|
||||
callback({ didTimeout: false, timeRemaining: () => 0 });
|
||||
return 1;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'cancelIdleCallback', {
|
||||
configurable: true,
|
||||
value: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
testUnivers.splice(0).forEach((univer) => univer.dispose());
|
||||
if (scrollIntoViewDescriptor) {
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', scrollIntoViewDescriptor);
|
||||
} else {
|
||||
delete (Element.prototype as { scrollIntoView?: unknown }).scrollIntoView;
|
||||
}
|
||||
|
||||
if (requestIdleCallbackDescriptor) {
|
||||
Object.defineProperty(globalThis, 'requestIdleCallback', requestIdleCallbackDescriptor);
|
||||
} else {
|
||||
delete (globalThis as { requestIdleCallback?: unknown }).requestIdleCallback;
|
||||
}
|
||||
|
||||
if (cancelIdleCallbackDescriptor) {
|
||||
Object.defineProperty(globalThis, 'cancelIdleCallback', cancelIdleCallbackDescriptor);
|
||||
} else {
|
||||
delete (globalThis as { cancelIdleCallback?: unknown }).cancelIdleCallback;
|
||||
}
|
||||
});
|
||||
|
||||
it('filters the popup menu by the typed keyword and closes the popup after selecting a business menu item', async () => {
|
||||
const { getCutContentParams, getSelectedMenu, injector, model, popupManagerService, popupService } = createQuickInsertPopupTestBed();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<QuickInsertPopup />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain('Table');
|
||||
expect(container.textContent).not.toContain('Image');
|
||||
|
||||
const tableMenuItem = Array.from(container.querySelectorAll<HTMLElement>('[role="button"]'))
|
||||
.find((node) => node.textContent === 'Table');
|
||||
|
||||
expect(tableMenuItem).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
tableMenuItem!.click();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(getSelectedMenu()).toEqual({
|
||||
id: 'insert-table',
|
||||
title: 'Table',
|
||||
keywords: ['table', 'grid', 'table'],
|
||||
});
|
||||
expect(getCutContentParams()).toEqual(expect.objectContaining({
|
||||
selections: [expect.objectContaining({ startOffset: 0, endOffset: 3 })],
|
||||
}));
|
||||
expect(popupService.editPopup).toBeNull();
|
||||
expect(popupManagerService.popups).toHaveLength(0);
|
||||
expect(model.getBody()?.dataStream).toBe('\r\n');
|
||||
});
|
||||
|
||||
it('uses popup keyboard commands to move focus and select the focused business menu item', async () => {
|
||||
const { getCutContentParams, getSelectedMenu, injector, popupService } = createQuickInsertPopupTestBed({
|
||||
dataStream: '/\r\n',
|
||||
inputOffset: { start: 0, end: 1 },
|
||||
});
|
||||
const commandService = injector.get(ICommandService);
|
||||
const shortcutService = injector.get(IShortcutService);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<QuickInsertPopup />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const moveDownShortcut = shortcutService.getAllShortcuts().find((shortcut) => shortcut.binding === KeyCode.ARROW_DOWN);
|
||||
const enterShortcut = shortcutService.getAllShortcuts().find((shortcut) => shortcut.binding === KeyCode.ENTER);
|
||||
|
||||
expect(moveDownShortcut).toBeDefined();
|
||||
expect(enterShortcut).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
await commandService.executeCommand(moveDownShortcut!.id, moveDownShortcut!.staticParameters);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await commandService.executeCommand(enterShortcut!.id);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(getSelectedMenu()).toEqual({
|
||||
id: 'insert-image',
|
||||
title: 'Image',
|
||||
keywords: ['image', 'picture', 'image'],
|
||||
});
|
||||
expect(getCutContentParams()).toEqual(expect.objectContaining({
|
||||
selections: [expect.objectContaining({ startOffset: 0, endOffset: 1 })],
|
||||
}));
|
||||
expect(popupService.editPopup).toBeNull();
|
||||
});
|
||||
|
||||
it('temporarily disables document cursor shortcuts while the popup owns keyboard navigation', async () => {
|
||||
const { injector } = createQuickInsertPopupTestBed({
|
||||
dataStream: '/\r\n',
|
||||
inputOffset: { start: 0, end: 1 },
|
||||
});
|
||||
const shortcutService = injector.get(IShortcutService);
|
||||
const documentArrowDownShortcut = {
|
||||
id: 'doc.operation.move-cursor-down',
|
||||
binding: KeyCode.ARROW_DOWN,
|
||||
preconditions: () => true,
|
||||
};
|
||||
|
||||
shortcutService.registerShortcut(documentArrowDownShortcut);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<QuickInsertPopup />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(documentArrowDownShortcut.preconditions()).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
root.render(<RediContext.Provider value={{ injector }} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(documentArrowDownShortcut.preconditions()).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the typed trigger intact when enter is pressed with no matching menu', async () => {
|
||||
const { getCutContentParams, getSelectedMenu, injector, popupService } = createQuickInsertPopupTestBed({
|
||||
dataStream: '/unknown\r\n',
|
||||
inputOffset: { start: 0, end: 8 },
|
||||
});
|
||||
const commandService = injector.get(ICommandService);
|
||||
const shortcutService = injector.get(IShortcutService);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<QuickInsertPopup />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain('No result');
|
||||
expect(container.textContent).not.toContain('Table');
|
||||
|
||||
const enterShortcut = shortcutService.getAllShortcuts().find((shortcut) => shortcut.binding === KeyCode.ENTER);
|
||||
expect(enterShortcut).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
await commandService.executeCommand(enterShortcut!.id);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(getSelectedMenu()).toBeUndefined();
|
||||
expect(getCutContentParams()).toBeUndefined();
|
||||
expect(popupService.editPopup).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocPopupMenu, IDocPopupMenuItem } from '../../services/doc-quick-insert-popup.service';
|
||||
import { Button, clsx, scrollbarClassName } from '@univerjs/design';
|
||||
import { IconManager, useDependency } from '@univerjs/ui';
|
||||
import { useQuickInsertPopup } from '../use-quick-insert-popup';
|
||||
|
||||
function MobileQuickInsertItems({ menus, onSelect }: { menus: DocPopupMenu[]; onSelect: (menu: IDocPopupMenuItem) => void }) {
|
||||
const iconManager = useDependency(IconManager);
|
||||
return menus.map((menu) => {
|
||||
const Icon = menu.icon ? iconManager.get(menu.icon) : null;
|
||||
if ('children' in menu) {
|
||||
return (
|
||||
<section key={menu.id} className="univer-grid univer-gap-1 univer-py-2">
|
||||
<div className="univer-px-3 univer-text-sm univer-font-medium">{menu.title}</div>
|
||||
<MobileQuickInsertItems menus={menu.children ?? []} onSelect={onSelect} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={menu.id}
|
||||
variant="text"
|
||||
className="univer-min-h-12 univer-w-full univer-justify-start univer-gap-2 univer-px-3"
|
||||
onClick={() => onSelect(menu)}
|
||||
>
|
||||
{Icon && <Icon />}
|
||||
<span className="univer-truncate">{menu.title}</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function MobileQuickInsertPopup() {
|
||||
const { filteredMenus, handleMenuSelect, Placeholder } = useQuickInsertPopup();
|
||||
return (
|
||||
<div className="univer-mt-1">
|
||||
{filteredMenus.length > 0
|
||||
? (
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-box-border univer-max-h-[min(42vh,360px)] univer-w-[min(360px,calc(100vw-24px))]
|
||||
univer-overflow-y-auto univer-overscroll-contain univer-rounded-2xl univer-bg-gray-0
|
||||
univer-p-2 univer-shadow-md
|
||||
dark:!univer-bg-gray-700
|
||||
`, scrollbarClassName)}
|
||||
onWheel={(event) => event.stopPropagation()}
|
||||
>
|
||||
<MobileQuickInsertItems menus={filteredMenus} onSelect={handleMenuSelect} />
|
||||
</div>
|
||||
)
|
||||
: Placeholder && <Placeholder />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocPopupMenu, IDocPopupMenuItem } from '../services/doc-quick-insert-popup.service';
|
||||
import {
|
||||
ICommandService,
|
||||
LocaleService,
|
||||
} from '@univerjs/core';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useMemo } from 'react';
|
||||
import { CloseQuickInsertPopupOperation } from '../commands/operations/quick-insert-popup.operation';
|
||||
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
|
||||
import { QuickInsertPlaceholder } from './QuickInsertPlaceholder';
|
||||
|
||||
function filterMenusByKeyword(menus: DocPopupMenu[], keyword: string) {
|
||||
return menus
|
||||
.map((menu) => ({ ...menu }))
|
||||
.filter((menu) => {
|
||||
if ('children' in menu) {
|
||||
menu.children = filterMenusByKeyword(menu.children!, keyword) as IDocPopupMenuItem[];
|
||||
|
||||
return menu.children.length > 0;
|
||||
}
|
||||
|
||||
const keywords = (menu as IDocPopupMenuItem).keywords;
|
||||
|
||||
if (keywords) {
|
||||
return keywords.some((word) => word.includes(keyword));
|
||||
}
|
||||
|
||||
return menu.title.toLowerCase().includes(keyword);
|
||||
});
|
||||
}
|
||||
|
||||
function translateMenus(menus: DocPopupMenu[], localeService: LocaleService) {
|
||||
return menus.map((_menu) => {
|
||||
const menu = { ..._menu } as DocPopupMenu;
|
||||
if ('children' in menu) {
|
||||
menu.children = translateMenus(menu.children!, localeService) as IDocPopupMenuItem[];
|
||||
}
|
||||
|
||||
menu.title = localeService.t(menu.title);
|
||||
|
||||
if ('keywords' in menu) {
|
||||
menu.keywords = menu.keywords!
|
||||
.concat(menu.title)
|
||||
.map((word) => word.toLowerCase());
|
||||
}
|
||||
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
|
||||
export function useQuickInsertPopup() {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const docQuickInsertPopupService = useDependency(DocQuickInsertPopupService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const locale = useObservable(localeService.currentLocale$);
|
||||
const filterKeyword = useObservable(docQuickInsertPopupService.filterKeyword$, '');
|
||||
const currentPopup = useObservable(docQuickInsertPopupService.editPopup$);
|
||||
const menus = useObservable<DocPopupMenu[]>(currentPopup?.popup.menus$, []);
|
||||
const filteredMenus = useMemo(
|
||||
() => filterMenusByKeyword(translateMenus(menus, localeService), filterKeyword.toLowerCase()),
|
||||
[menus, localeService, locale, filterKeyword]
|
||||
);
|
||||
const handleMenuSelect = (menu: IDocPopupMenuItem) => {
|
||||
docQuickInsertPopupService.emitMenuSelected(menu);
|
||||
commandService.executeCommand(CloseQuickInsertPopupOperation.id);
|
||||
};
|
||||
const Placeholder = currentPopup?.popup.Placeholder || QuickInsertPlaceholder;
|
||||
return { filteredMenus, handleMenuSelect, Placeholder };
|
||||
}
|
||||
27
packages/docs-quick-insert-ui/tailwind.config.ts
Normal file
27
packages/docs-quick-insert-ui/tailwind.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Config } from 'tailwindcss';
|
||||
import preset from '@univerjs-infra/shared/tailwind';
|
||||
|
||||
const config: Config = {
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
packages/docs-quick-insert-ui/tsconfig.json
Normal file
8
packages/docs-quick-insert-ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/docs-quick-insert-ui/tsconfig.node.json
Normal file
18
packages/docs-quick-insert-ui/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/docs-quick-insert-ui/vitest.config.ts
Normal file
3
packages/docs-quick-insert-ui/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user