mirror of https://github.com/opensumi/core
style: use eslint and prettier (#138)
* style: use prettier format once * style: few eslint fix * style: fix eslint * style: update eslint rule * chore: resolve conflict, fix some bug * style: fix lint * style: fix eslint * style: run prettier Co-authored-by: Aaaaash <binshao54@gmail.com>
This commit is contained in:
parent
1b9298145b
commit
8a43372945
|
@ -9,3 +9,10 @@ tools/cli-engine/src/browser/worker-host.js
|
|||
**/scripts/**
|
||||
packages/*/lib/**
|
||||
packages/*/dist/**
|
||||
|
||||
packages/components/src/icon/iconfont
|
||||
packages/core-browser/src/style/octicons
|
||||
|
||||
packages/extension/__mocks__/extension/browser-new.js
|
||||
packages/extension/__mocks__/extension/browser.js
|
||||
packages/extension/__mocks__/extension-error/browser.js
|
||||
|
|
41
.eslintrc.js
41
.eslintrc.js
|
@ -72,7 +72,6 @@ module.exports = {
|
|||
'@typescript-eslint/unified-signatures': 'error',
|
||||
'arrow-body-style': 'error',
|
||||
'arrow-parens': ['error', 'always'],
|
||||
'brace-style': ['error', '1tbs'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
complexity: 'off',
|
||||
'constructor-super': 'error',
|
||||
|
@ -80,23 +79,7 @@ module.exports = {
|
|||
'eol-last': 'error',
|
||||
eqeqeq: ['error', 'smart'],
|
||||
'guard-for-in': 'error',
|
||||
'id-blacklist': [
|
||||
'error',
|
||||
'any',
|
||||
'Number',
|
||||
'number',
|
||||
'String',
|
||||
'string',
|
||||
'Boolean',
|
||||
'boolean',
|
||||
'Undefined',
|
||||
'undefined',
|
||||
],
|
||||
'id-match': 'error',
|
||||
'import/order': 'off',
|
||||
'jsdoc/check-alignment': 'off',
|
||||
'jsdoc/check-indentation': 'off',
|
||||
'jsdoc/newline-after-description': 'off',
|
||||
'max-classes-per-file': 'off',
|
||||
'max-len': 'off',
|
||||
'new-parens': 'error',
|
||||
|
@ -105,6 +88,7 @@ module.exports = {
|
|||
'no-cond-assign': 'off',
|
||||
'no-console': 'off',
|
||||
'no-debugger': 'error',
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
// We strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// The checks it provides are already provided by TypeScript without the need for configuration
|
||||
// TypeScript just does this significantly better.
|
||||
|
@ -123,17 +107,8 @@ module.exports = {
|
|||
'object-shorthand': 'error',
|
||||
'one-var': ['error', 'never'],
|
||||
'prefer-arrow/prefer-arrow-functions': 'off',
|
||||
'prefer-const': 'error',
|
||||
'quote-props': 'off',
|
||||
radix: 'error',
|
||||
'space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'never',
|
||||
asyncArrow: 'always',
|
||||
named: 'never',
|
||||
},
|
||||
],
|
||||
'spaced-comment': [
|
||||
'error',
|
||||
'always',
|
||||
|
@ -143,5 +118,19 @@ module.exports = {
|
|||
],
|
||||
'use-isnan': 'error',
|
||||
'valid-typeof': 'off',
|
||||
'no-irregular-whitespace': ['error', { skipComments: true }],
|
||||
'no-inner-declarations': 'off',
|
||||
'no-useless-catch': 'warn',
|
||||
// TODO: should set below to error in future
|
||||
'no-useless-escape': 'warn',
|
||||
'no-async-promise-executor': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
'@typescript-eslint/no-this-alias': 'warn',
|
||||
'@typescript-eslint/ban-types': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'prefer-rest-params': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
node_modules
|
||||
|
||||
**/tools/workspaces/**
|
||||
**/tools/extensions/**
|
||||
**/tools/**/vendor/
|
||||
tools/cli-engine/src/browser/worker-host.js
|
||||
|
||||
**/typings/**
|
||||
**/scripts/**
|
||||
packages/*/lib/**
|
||||
packages/*/dist/**
|
||||
|
||||
packages/components/src/icon/iconfont
|
||||
packages/core-browser/src/style/octicons
|
||||
|
||||
packages/extension/__mocks__/extension/browser-new.js
|
||||
packages/extension/__mocks__/extension/browser.js
|
||||
packages/extension/__mocks__/extension-error/browser.js
|
|
@ -18,7 +18,7 @@ module.exports = {
|
|||
'!packages/**/*-contribution.ts',
|
||||
'!packages/startup/**/*.ts',
|
||||
// Test 功能暂未完成
|
||||
"!packages/testing/**/*.ts",
|
||||
'!packages/testing/**/*.ts',
|
||||
'!packages/core-electron-main/**/*.ts',
|
||||
'!packages/*/src/electron-main/**/*.ts',
|
||||
],
|
||||
|
|
12
package.json
12
package.json
|
@ -30,10 +30,9 @@
|
|||
"publish:engine": "sh ./scripts/release-cli-engine.sh",
|
||||
"update-version": "ts-node ./scripts/publish --versionOnly",
|
||||
"update-disttag": "ts-node ./scripts/dist-tag",
|
||||
"lint": "tslint -c tslint.yml -p tsconfig.json -t stylish",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"lint1": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint1:fix": "npm run lint1 -- --fix",
|
||||
"format": "npm run lint:fix && prettier \"**/*.{js,jsx,ts,tsx,html,css,less}\" --write",
|
||||
"rebuild:node": "node ./scripts/rebuild-native.js",
|
||||
"test": "jest --forceExit",
|
||||
"test:module": "NODE_OPTIONS=--max_old_space_size=5120 node -r ts-node/register ./scripts/module-jest",
|
||||
|
@ -63,16 +62,12 @@
|
|||
"lodash": "^4.17.11",
|
||||
"node-gyp": "^8.3.0",
|
||||
"ts-node": "8.0.2",
|
||||
"tslint": "^5.12.0",
|
||||
"typescript": "^4.4.2",
|
||||
"webpack": "4.39.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,html,css,less,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": "eslint --fix",
|
||||
"packages/**/*.(ts|tsx)": [
|
||||
"tslint -c tslint.staged.yml -p tsconfig.json -t stylish"
|
||||
]
|
||||
"*.{js,jsx,ts,tsx}": "eslint --fix --quiet"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
@ -149,7 +144,6 @@
|
|||
"strip-html-comments": "^1.0.0",
|
||||
"temp": "^0.9.0",
|
||||
"ts-jest": "^27.0.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"urllib": "^2.37.4",
|
||||
"uuid": "^8.3.2",
|
||||
"write-pkg": "^4.0.0",
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
import * as modes from '@opensumi/monaco-editor-core/esm/vs/editor/common/modes';
|
||||
import { CommandService, CommandServiceImpl, CommandRegistryImpl, CommandRegistry, DisposableCollection } from '@opensumi/ide-core-common';
|
||||
import { KeybindingRegistry, KeybindingRegistryImpl, RecentFilesManager, ILogger, PreferenceService } from '@opensumi/ide-core-browser';
|
||||
import {
|
||||
CommandService,
|
||||
CommandServiceImpl,
|
||||
CommandRegistryImpl,
|
||||
CommandRegistry,
|
||||
DisposableCollection,
|
||||
} from '@opensumi/ide-core-common';
|
||||
import {
|
||||
KeybindingRegistry,
|
||||
KeybindingRegistryImpl,
|
||||
RecentFilesManager,
|
||||
ILogger,
|
||||
PreferenceService,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||
import { PrefixQuickOpenService } from '@opensumi/ide-quick-open';
|
||||
import { QuickOpenHandlerRegistry } from '@opensumi/ide-quick-open/lib/browser/prefix-quick-open.service';
|
||||
|
@ -13,7 +25,13 @@ import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-h
|
|||
import { MockInjector } from '../../../../tools/dev-tool/src/mock-injector';
|
||||
|
||||
import { ClientAddonModule } from '../../src/browser';
|
||||
import { FileSearchContribution, quickFileOpen, FileSearchQuickCommandHandler, matchLineReg, getValidateInput } from '../../src/browser/file-search.contribution';
|
||||
import {
|
||||
FileSearchContribution,
|
||||
quickFileOpen,
|
||||
FileSearchQuickCommandHandler,
|
||||
matchLineReg,
|
||||
getValidateInput,
|
||||
} from '../../src/browser/file-search.contribution';
|
||||
|
||||
describe('test for browser/file-search.contribution.ts', () => {
|
||||
let injector: MockInjector;
|
||||
|
@ -22,27 +40,34 @@ describe('test for browser/file-search.contribution.ts', () => {
|
|||
const disposables = new DisposableCollection();
|
||||
|
||||
beforeEach(() => {
|
||||
injector = createBrowserInjector([ ClientAddonModule ], new MockInjector([
|
||||
{
|
||||
token: CommandRegistry,
|
||||
useClass: CommandRegistryImpl,
|
||||
}, {
|
||||
token: CommandService,
|
||||
useClass: CommandServiceImpl,
|
||||
}, {
|
||||
token: KeybindingRegistry,
|
||||
useClass: KeybindingRegistryImpl,
|
||||
}, {
|
||||
token: FileSearchQuickCommandHandler,
|
||||
useValue: {},
|
||||
}, {
|
||||
token: PrefixQuickOpenService,
|
||||
useValue: {
|
||||
open: fakeOpenFn,
|
||||
injector = createBrowserInjector(
|
||||
[ClientAddonModule],
|
||||
new MockInjector([
|
||||
{
|
||||
token: CommandRegistry,
|
||||
useClass: CommandRegistryImpl,
|
||||
},
|
||||
},
|
||||
QuickOpenHandlerRegistry,
|
||||
]));
|
||||
{
|
||||
token: CommandService,
|
||||
useClass: CommandServiceImpl,
|
||||
},
|
||||
{
|
||||
token: KeybindingRegistry,
|
||||
useClass: KeybindingRegistryImpl,
|
||||
},
|
||||
{
|
||||
token: FileSearchQuickCommandHandler,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
token: PrefixQuickOpenService,
|
||||
useValue: {
|
||||
open: fakeOpenFn,
|
||||
},
|
||||
},
|
||||
QuickOpenHandlerRegistry,
|
||||
]),
|
||||
);
|
||||
|
||||
// 获取对象实例的时候才开始注册事件
|
||||
contribution = injector.get(FileSearchContribution);
|
||||
|
@ -121,7 +146,6 @@ describe('test for browser/file-search.contribution.ts', () => {
|
|||
});
|
||||
|
||||
describe('file-search-quickopen', () => {
|
||||
|
||||
let injector: MockInjector;
|
||||
let fileSearchQuickOpenHandler: FileSearchQuickCommandHandler;
|
||||
|
||||
|
@ -168,13 +192,11 @@ describe('file-search-quickopen', () => {
|
|||
},
|
||||
];
|
||||
|
||||
modes.DocumentSymbolProviderRegistry['all'] = () => {
|
||||
return [{
|
||||
provideDocumentSymbols: () => {
|
||||
return testDS;
|
||||
},
|
||||
}];
|
||||
};
|
||||
modes.DocumentSymbolProviderRegistry['all'] = () => [
|
||||
{
|
||||
provideDocumentSymbols: () => testDS,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = createBrowserInjector([]);
|
||||
|
@ -187,10 +209,7 @@ describe('file-search-quickopen', () => {
|
|||
{
|
||||
token: FileSearchServicePath,
|
||||
useValue: {
|
||||
find: () => [
|
||||
'/file/a',
|
||||
'/file/b',
|
||||
],
|
||||
find: () => ['/file/a', '/file/b'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -214,20 +233,16 @@ describe('file-search-quickopen', () => {
|
|||
injector.mockService(PreferenceService, {});
|
||||
injector.mockService(ILogger, {});
|
||||
injector.mockService(IEditorDocumentModelService, {
|
||||
createModelReference: (uri) => {
|
||||
return {
|
||||
instance: {
|
||||
createModelReference: (uri) => ({
|
||||
instance: {
|
||||
uri,
|
||||
getMonacoModel: () => ({
|
||||
uri,
|
||||
getMonacoModel: () => {
|
||||
return {
|
||||
uri,
|
||||
getLanguageIdentifier: () => 'javascript',
|
||||
};
|
||||
},
|
||||
},
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
},
|
||||
getLanguageIdentifier: () => 'javascript',
|
||||
}),
|
||||
},
|
||||
dispose: jest.fn(),
|
||||
}),
|
||||
});
|
||||
fileSearchQuickOpenHandler = injector.get(FileSearchQuickCommandHandler);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { CommandService, IEventBus, EventBusImpl, BrowserConnectionCloseEvent, BrowserConnectionOpenEvent } from '@opensumi/ide-core-common';
|
||||
import {
|
||||
CommandService,
|
||||
IEventBus,
|
||||
EventBusImpl,
|
||||
BrowserConnectionCloseEvent,
|
||||
BrowserConnectionOpenEvent,
|
||||
} from '@opensumi/ide-core-common';
|
||||
|
||||
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
|
||||
import { MockInjector } from '../../../../tools/dev-tool/src/mock-injector';
|
||||
|
@ -12,17 +18,21 @@ describe('test for browser/status-bar-contribution.ts', () => {
|
|||
const fakeExecCmd = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
injector = createBrowserInjector([ ClientAddonModule ], new MockInjector([
|
||||
{
|
||||
token: IEventBus,
|
||||
useClass: EventBusImpl,
|
||||
}, {
|
||||
token: CommandService,
|
||||
useValue: {
|
||||
executeCommand: fakeExecCmd,
|
||||
injector = createBrowserInjector(
|
||||
[ClientAddonModule],
|
||||
new MockInjector([
|
||||
{
|
||||
token: IEventBus,
|
||||
useClass: EventBusImpl,
|
||||
},
|
||||
},
|
||||
]));
|
||||
{
|
||||
token: CommandService,
|
||||
useValue: {
|
||||
executeCommand: fakeExecCmd,
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
eventBus = injector.get(IEventBus);
|
||||
// 获取对象实例的时候才开始注册事件
|
||||
|
|
|
@ -7,9 +7,7 @@ describe('test for ', () => {
|
|||
let injector: MockInjector;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = createNodeInjector([
|
||||
AddonsModule,
|
||||
]);
|
||||
injector = createNodeInjector([AddonsModule]);
|
||||
});
|
||||
|
||||
it('empty module', () => {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { Injectable, Autowired } from '@opensumi/di';
|
||||
import { ClientAppContribution, Domain } from '@opensumi/ide-core-browser';
|
||||
import { debounce, IReporterService, StaleLRUMap, OnEvent, URI, WithEventBus } from '@opensumi/ide-core-common';
|
||||
import { FileOperation, WorkspaceFileEvent, IWorkspaceFileOperationParticipant, IWorkspaceFileService } from '@opensumi/ide-workspace-edit';
|
||||
import {
|
||||
FileOperation,
|
||||
WorkspaceFileEvent,
|
||||
IWorkspaceFileOperationParticipant,
|
||||
IWorkspaceFileService,
|
||||
} from '@opensumi/ide-workspace-edit';
|
||||
import { PreferenceSchema, PreferenceSchemaProvider, PreferenceService } from '@opensumi/ide-core-browser';
|
||||
import { EditorDocumentModelSavedEvent, EditorDocumentModelWillSaveEvent } from '@opensumi/ide-editor/lib/browser';
|
||||
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||
|
@ -92,7 +97,7 @@ export class FileAndContentUpdateTimeContribution extends WithEventBus {
|
|||
@Autowired(PreferenceSchemaProvider)
|
||||
private readonly preferenceSchemaProvider: PreferenceSchemaProvider;
|
||||
|
||||
private _traceConfig: boolean = false;
|
||||
private _traceConfig = false;
|
||||
|
||||
private _markedFileUris = new StaleLRUMap<string, FileChangeMarker>(100, 50, 10 * 60 * 1000 /* 十分钟超时清理 */);
|
||||
|
||||
|
@ -136,14 +141,14 @@ export class FileAndContentUpdateTimeContribution extends WithEventBus {
|
|||
);
|
||||
|
||||
// AFTER file operation SUCCEED
|
||||
this.addDispose(this.workspaceFileService.onDidRunWorkspaceFileOperation(
|
||||
this._handleFileOperationDidRun.bind(this),
|
||||
));
|
||||
this.addDispose(
|
||||
this.workspaceFileService.onDidRunWorkspaceFileOperation(this._handleFileOperationDidRun.bind(this)),
|
||||
);
|
||||
|
||||
// AFTER file operation FAILED
|
||||
this.addDispose(this.workspaceFileService.onDidFailWorkspaceFileOperation(
|
||||
this._handleFileOperationDidFail.bind(this),
|
||||
));
|
||||
this.addDispose(
|
||||
this.workspaceFileService.onDidFailWorkspaceFileOperation(this._handleFileOperationDidFail.bind(this)),
|
||||
);
|
||||
}
|
||||
|
||||
private async fileOperationParticipant(...args: Parameters<IWorkspaceFileOperationParticipant['participate']>) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { OnEvent, FileTreeDropEvent, WithEventBus } from '@opensumi/ide-core-com
|
|||
@Injectable()
|
||||
@Domain(ClientAppContribution)
|
||||
export class FileDropContribution extends WithEventBus {
|
||||
|
||||
@Autowired(IFileDropFrontendServiceToken)
|
||||
protected readonly dropService: IFileDropFrontendService;
|
||||
|
||||
|
|
|
@ -3,13 +3,18 @@ import { WithEventBus } from '@opensumi/ide-core-common/lib';
|
|||
import { Uri, formatLocalize } from '@opensumi/ide-core-browser/lib';
|
||||
import { FileTreeDropEvent } from '@opensumi/ide-core-common/lib/types/dnd';
|
||||
import { IStatusBarService, StatusBarAlignment, StatusBarEntryAccessor } from '@opensumi/ide-core-browser/lib/services';
|
||||
import { IFileDropFrontendService, IFileDropBackendService, FileDropServicePath, IWebkitDataTransfer, IWebkitDataTransferItemEntry } from '../common';
|
||||
import {
|
||||
IFileDropFrontendService,
|
||||
IFileDropBackendService,
|
||||
FileDropServicePath,
|
||||
IWebkitDataTransfer,
|
||||
IWebkitDataTransferItemEntry,
|
||||
} from '../common';
|
||||
import { Path } from '@opensumi/ide-components/lib/utils/path';
|
||||
import { IFileServiceClient } from '@opensumi/ide-file-service/lib/common';
|
||||
|
||||
@Injectable()
|
||||
export class FileDropService extends WithEventBus implements IFileDropFrontendService {
|
||||
|
||||
private pending: Set<string> = new Set();
|
||||
|
||||
@Autowired(IFileServiceClient)
|
||||
|
@ -53,13 +58,15 @@ export class FileDropService extends WithEventBus implements IFileDropFrontendSe
|
|||
|
||||
if (!this.uploadStatus) {
|
||||
this.uploadStatus = this.statusBarService.addElement(entryId, entry);
|
||||
} else {
|
||||
} else {
|
||||
this.uploadStatus.update({ id: entryId, ...entry });
|
||||
}
|
||||
}
|
||||
|
||||
onDidDropFile(e: FileTreeDropEvent) {
|
||||
const { payload: { event, targetDir } } = e;
|
||||
const {
|
||||
payload: { event, targetDir },
|
||||
} = e;
|
||||
if (!targetDir || !event.dataTransfer?.files || event.dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -82,7 +89,11 @@ export class FileDropService extends WithEventBus implements IFileDropFrontendSe
|
|||
}
|
||||
}
|
||||
|
||||
private async processFilesEntry(targetDir: string, entry: IWebkitDataTransferItemEntry, reporter: (uploadedByteLength: number) => void) {
|
||||
private async processFilesEntry(
|
||||
targetDir: string,
|
||||
entry: IWebkitDataTransferItemEntry,
|
||||
reporter: (uploadedByteLength: number) => void,
|
||||
) {
|
||||
if (entry.isFile) {
|
||||
this.processFileEntry(entry, targetDir, reporter);
|
||||
} else {
|
||||
|
@ -120,20 +131,33 @@ export class FileDropService extends WithEventBus implements IFileDropFrontendSe
|
|||
}
|
||||
}
|
||||
|
||||
private processFileEntry(fileEntry: IWebkitDataTransferItemEntry, targetDir: string, reporter: (uploadedByteLength: number) => void): void {
|
||||
fileEntry.file((fileChunk) => {
|
||||
const file = new File([fileChunk], fileEntry.fullPath!, { type: fileEntry.type! });
|
||||
this.doUploadFile(file, targetDir, reporter);
|
||||
}, () => {});
|
||||
private processFileEntry(
|
||||
fileEntry: IWebkitDataTransferItemEntry,
|
||||
targetDir: string,
|
||||
reporter: (uploadedByteLength: number) => void,
|
||||
): void {
|
||||
fileEntry.file(
|
||||
(fileChunk) => {
|
||||
const file = new File([fileChunk], fileEntry.fullPath!, { type: fileEntry.type! });
|
||||
this.doUploadFile(file, targetDir, reporter);
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
private processDirEntry(entry: IWebkitDataTransferItemEntry, targetDir: string, reporter: (uploadedByteLength: number) => void) {
|
||||
private processDirEntry(
|
||||
entry: IWebkitDataTransferItemEntry,
|
||||
targetDir: string,
|
||||
reporter: (uploadedByteLength: number) => void,
|
||||
) {
|
||||
const dirReader = entry.createReader();
|
||||
dirReader.readEntries((entries) => {
|
||||
entries.forEach(async (fileOrDirEntry) => {
|
||||
this.processFilesEntry(targetDir, fileOrDirEntry, reporter);
|
||||
});
|
||||
}, () => {});
|
||||
dirReader.readEntries(
|
||||
(entries) => {
|
||||
entries.forEach(async (fileOrDirEntry) => {
|
||||
this.processFilesEntry(targetDir, fileOrDirEntry, reporter);
|
||||
});
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -32,11 +32,23 @@ import {
|
|||
import { LabelService } from '@opensumi/ide-core-browser/lib/services';
|
||||
import { KeybindingContribution, KeybindingRegistry, ILogger } from '@opensumi/ide-core-browser';
|
||||
import { Domain } from '@opensumi/ide-core-common/lib/di-helper';
|
||||
import { QuickOpenContribution, QuickOpenHandlerRegistry } from '@opensumi/ide-quick-open/lib/browser/prefix-quick-open.service';
|
||||
import { QuickOpenModel, QuickOpenOptions, PrefixQuickOpenService, QuickOpenBaseAction } from '@opensumi/ide-quick-open';
|
||||
import {
|
||||
QuickOpenContribution,
|
||||
QuickOpenHandlerRegistry,
|
||||
} from '@opensumi/ide-quick-open/lib/browser/prefix-quick-open.service';
|
||||
import {
|
||||
QuickOpenModel,
|
||||
QuickOpenOptions,
|
||||
PrefixQuickOpenService,
|
||||
QuickOpenBaseAction,
|
||||
} from '@opensumi/ide-quick-open';
|
||||
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||
import { EditorGroupSplitAction, WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||
import { DocumentSymbolStore, IDummyRoot, INormalizedDocumentSymbol } from '@opensumi/ide-editor/lib/browser/breadcrumb/document-symbol';
|
||||
import {
|
||||
DocumentSymbolStore,
|
||||
IDummyRoot,
|
||||
INormalizedDocumentSymbol,
|
||||
} from '@opensumi/ide-editor/lib/browser/breadcrumb/document-symbol';
|
||||
import { getIcon } from '@opensumi/ide-core-browser';
|
||||
import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search/lib/common';
|
||||
import { RecentFilesManager } from '@opensumi/ide-core-browser';
|
||||
|
@ -62,7 +74,7 @@ export const quickGoToSymbol: Command = {
|
|||
// support /some/file.js:73:84
|
||||
export const matchLineReg = /^([^:#\(]*)[:#\(]?L?(\d+)?[:,]?(\d+)?\)?/;
|
||||
|
||||
function getRangeByInput(input: string = ''): monaco.Range | undefined {
|
||||
function getRangeByInput(input = ''): monaco.Range | undefined {
|
||||
const matchList = input.match(matchLineReg) || [];
|
||||
|
||||
if (matchList.length < 2) {
|
||||
|
@ -74,12 +86,7 @@ function getRangeByInput(input: string = ''): monaco.Range | undefined {
|
|||
start: Number(matchList[3] || 0),
|
||||
};
|
||||
|
||||
return new monaco.Range(
|
||||
lineInfo.line,
|
||||
lineInfo.start,
|
||||
lineInfo.line,
|
||||
lineInfo.start,
|
||||
);
|
||||
return new monaco.Range(lineInfo.line, lineInfo.start, lineInfo.line, lineInfo.start);
|
||||
}
|
||||
|
||||
export function getValidateInput(input: string) {
|
||||
|
@ -88,7 +95,6 @@ export function getValidateInput(input: string) {
|
|||
|
||||
@Injectable()
|
||||
class FileSearchActionLeftRight extends QuickOpenBaseAction {
|
||||
|
||||
@Autowired(CommandService)
|
||||
private readonly commandService: CommandService;
|
||||
|
||||
|
@ -115,7 +121,6 @@ class FileSearchActionLeftRight extends QuickOpenBaseAction {
|
|||
|
||||
@Injectable()
|
||||
class FileSearchActionProvider implements QuickOpenActionProvider {
|
||||
|
||||
@Autowired()
|
||||
private readonly fileSearchActionLeftRight: FileSearchActionLeftRight;
|
||||
|
||||
|
@ -134,7 +139,6 @@ class FileSearchActionProvider implements QuickOpenActionProvider {
|
|||
|
||||
@Injectable()
|
||||
export class FileSearchQuickCommandHandler {
|
||||
|
||||
@Autowired(CommandService)
|
||||
private readonly commandService: CommandService;
|
||||
|
||||
|
@ -172,10 +176,10 @@ export class FileSearchQuickCommandHandler {
|
|||
readonly default: boolean = true;
|
||||
readonly prefix: string = '...';
|
||||
readonly description: string = localize('file-search.command.fileOpen.description');
|
||||
private prevEditorState: { uri?: URI, range?: IRange } = {};
|
||||
private prevEditorState: { uri?: URI; range?: IRange } = {};
|
||||
private prevSelected: URI | undefined;
|
||||
|
||||
currentLookFor: string = '';
|
||||
currentLookFor = '';
|
||||
|
||||
getModel(): QuickOpenModel {
|
||||
return {
|
||||
|
@ -225,10 +229,11 @@ export class FileSearchQuickCommandHandler {
|
|||
fuzzyMatchDescription: {
|
||||
enableSeparateSubstringMatching: true,
|
||||
},
|
||||
getPlaceholderItem: (lookFor: string) => new QuickOpenItem({
|
||||
label: localize(lookFor.indexOf('@') > -1 ? 'fileSymbolResults.notfound' : 'fileResults.notfound'),
|
||||
run: () => false,
|
||||
}),
|
||||
getPlaceholderItem: (lookFor: string) =>
|
||||
new QuickOpenItem({
|
||||
label: localize(lookFor.indexOf('@') > -1 ? 'fileSymbolResults.notfound' : 'fileResults.notfound'),
|
||||
run: () => false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -255,12 +260,7 @@ export class FileSearchQuickCommandHandler {
|
|||
let currentRange = { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 };
|
||||
const selections = this.workbenchEditorService.currentEditor?.getSelections();
|
||||
if (selections) {
|
||||
const {
|
||||
selectionStartLineNumber,
|
||||
selectionStartColumn,
|
||||
positionLineNumber,
|
||||
positionColumn,
|
||||
} = selections[0];
|
||||
const { selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn } = selections[0];
|
||||
currentRange = new monaco.Range(
|
||||
selectionStartLineNumber,
|
||||
selectionStartColumn,
|
||||
|
@ -297,39 +297,42 @@ export class FileSearchQuickCommandHandler {
|
|||
targetFile = this.workbenchEditorService.currentResource?.uri;
|
||||
}
|
||||
if (targetFile) {
|
||||
const symbols = await this.documentSymbolStore.getDocumentSymbolAsync(targetFile) || [];
|
||||
const symbols = (await this.documentSymbolStore.getDocumentSymbolAsync(targetFile)) || [];
|
||||
// 将symbol tree节点展开
|
||||
const flatSymbols: INormalizedDocumentSymbol[] = [];
|
||||
this.flattenSymbols({ children: symbols }, flatSymbols);
|
||||
const items: QuickOpenItem[] = flatSymbols.filter((item) => {
|
||||
// 手动匹配symbol并高亮
|
||||
const matchRange: Highlight[] = matchesFuzzy(symbolQuery, item.name, true) || [];
|
||||
if (matchRange) {
|
||||
(item as any).labelHighlights = matchRange;
|
||||
}
|
||||
return matchRange && matchRange.length;
|
||||
}).map((symbol, index) => {
|
||||
return new QuickOpenItem({
|
||||
uri: targetFile,
|
||||
label: symbol.name,
|
||||
iconClass: getSymbolIcon(symbol.kind),
|
||||
description: (symbol.parent as INormalizedDocumentSymbol)?.name,
|
||||
labelHighlights: (symbol as any).labelHighlights,
|
||||
groupLabel: index === 0 ? formatLocalize('fileSymbolResults', flatSymbols.length) : '',
|
||||
showBorder: false,
|
||||
run: (mode: Mode) => {
|
||||
if (mode === Mode.PREVIEW) {
|
||||
this.locateSymbol(targetFile!, symbol);
|
||||
return true;
|
||||
}
|
||||
if (mode === Mode.OPEN) {
|
||||
this.locateSymbol(targetFile!, symbol);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
});
|
||||
const items: QuickOpenItem[] = flatSymbols
|
||||
.filter((item) => {
|
||||
// 手动匹配symbol并高亮
|
||||
const matchRange: Highlight[] = matchesFuzzy(symbolQuery, item.name, true) || [];
|
||||
if (matchRange) {
|
||||
(item as any).labelHighlights = matchRange;
|
||||
}
|
||||
return matchRange && matchRange.length;
|
||||
})
|
||||
.map(
|
||||
(symbol, index) =>
|
||||
new QuickOpenItem({
|
||||
uri: targetFile,
|
||||
label: symbol.name,
|
||||
iconClass: getSymbolIcon(symbol.kind),
|
||||
description: (symbol.parent as INormalizedDocumentSymbol)?.name,
|
||||
labelHighlights: (symbol as any).labelHighlights,
|
||||
groupLabel: index === 0 ? formatLocalize('fileSymbolResults', flatSymbols.length) : '',
|
||||
showBorder: false,
|
||||
run: (mode: Mode) => {
|
||||
if (mode === Mode.PREVIEW) {
|
||||
this.locateSymbol(targetFile!, symbol);
|
||||
return true;
|
||||
}
|
||||
if (mode === Mode.OPEN) {
|
||||
this.locateSymbol(targetFile!, symbol);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
);
|
||||
results = items;
|
||||
} else {
|
||||
return [];
|
||||
|
@ -338,12 +341,10 @@ export class FileSearchQuickCommandHandler {
|
|||
results = await this.getQueryFiles(lookFor, alreadyCollected, token);
|
||||
// 排序后设置第一个元素的样式
|
||||
if (results[0]) {
|
||||
const newItems = await this.getItems(
|
||||
[results[0].getUri()!.toString()],
|
||||
{
|
||||
groupLabel: localize('fileResults'),
|
||||
showBorder: true,
|
||||
});
|
||||
const newItems = await this.getItems([results[0].getUri()!.toString()], {
|
||||
groupLabel: localize('fileResults'),
|
||||
showBorder: true,
|
||||
});
|
||||
results[0] = newItems[0];
|
||||
}
|
||||
}
|
||||
|
@ -361,19 +362,21 @@ export class FileSearchQuickCommandHandler {
|
|||
this.logger.debug('file-search.contribution rootUri', uri.toString());
|
||||
return rootUris.push(uri.toString());
|
||||
});
|
||||
const files = await this.fileSearchService.find(fileQuery, {
|
||||
rootUris,
|
||||
fuzzyMatch: true,
|
||||
limit: DEFAULT_FILE_SEARCH_LIMIT,
|
||||
useGitIgnore: true,
|
||||
noIgnoreParent: true,
|
||||
excludePatterns: ['*.git*', ...this.getPreferenceSearchExcludes()],
|
||||
}, token);
|
||||
const files = await this.fileSearchService.find(
|
||||
fileQuery,
|
||||
{
|
||||
rootUris,
|
||||
fuzzyMatch: true,
|
||||
limit: DEFAULT_FILE_SEARCH_LIMIT,
|
||||
useGitIgnore: true,
|
||||
noIgnoreParent: true,
|
||||
excludePatterns: ['*.git*', ...this.getPreferenceSearchExcludes()],
|
||||
},
|
||||
token,
|
||||
);
|
||||
const results = await this.getItems(
|
||||
files.filter((uri: string) => {
|
||||
if (alreadyCollected.has(uri) ||
|
||||
token.isCancellationRequested
|
||||
) {
|
||||
if (alreadyCollected.has(uri) || token.isCancellationRequested) {
|
||||
return false;
|
||||
}
|
||||
alreadyCollected.add(uri);
|
||||
|
@ -395,15 +398,12 @@ export class FileSearchQuickCommandHandler {
|
|||
}
|
||||
|
||||
private async getRecentlyItems(alreadyCollected, lookFor, token) {
|
||||
const recentlyOpenedFiles = await this.recentFilesManager.getMostRecentlyOpenedFiles(true) || [];
|
||||
const recentlyOpenedFiles = (await this.recentFilesManager.getMostRecentlyOpenedFiles(true)) || [];
|
||||
|
||||
return await this.getItems(
|
||||
recentlyOpenedFiles.filter((uri: string) => {
|
||||
const _uri = new URI(uri);
|
||||
if (alreadyCollected.has(uri) ||
|
||||
!fuzzy.test(lookFor, _uri.displayName) ||
|
||||
token.isCancellationRequested
|
||||
) {
|
||||
if (alreadyCollected.has(uri) || !fuzzy.test(lookFor, _uri.displayName) || token.isCancellationRequested) {
|
||||
return false;
|
||||
}
|
||||
alreadyCollected.add(uri);
|
||||
|
@ -415,10 +415,7 @@ export class FileSearchQuickCommandHandler {
|
|||
);
|
||||
}
|
||||
|
||||
private async getItems(
|
||||
uriList: string[],
|
||||
options: { [key: string]: any },
|
||||
) {
|
||||
private async getItems(uriList: string[], options: { [key: string]: any }) {
|
||||
const items: QuickOpenItem[] = [];
|
||||
|
||||
for (const [index, strUri] of uriList.entries()) {
|
||||
|
@ -432,7 +429,7 @@ export class FileSearchQuickCommandHandler {
|
|||
iconClass: icon,
|
||||
description,
|
||||
groupLabel: index === 0 ? options.groupLabel : '',
|
||||
showBorder: (uriList.length > 0 && index === 0) ? options.showBorder : false,
|
||||
showBorder: uriList.length > 0 && index === 0 ? options.showBorder : false,
|
||||
run: (mode: Mode) => {
|
||||
if (mode === Mode.PREVIEW) {
|
||||
this.prevSelected = uri;
|
||||
|
@ -457,7 +454,11 @@ export class FileSearchQuickCommandHandler {
|
|||
range = getRangeByInput(uri.fragment ? filePath + '#' + uri.fragment : filePath);
|
||||
}
|
||||
this.currentLookFor = '';
|
||||
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri.withoutFragment(), { preview: false, range, focus: true });
|
||||
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri.withoutFragment(), {
|
||||
preview: false,
|
||||
range,
|
||||
focus: true,
|
||||
});
|
||||
}
|
||||
|
||||
private locateSymbol(uri: URI, symbol: INormalizedDocumentSymbol) {
|
||||
|
@ -474,11 +475,7 @@ export class FileSearchQuickCommandHandler {
|
|||
* @param b `QuickOpenItem` for comparison.
|
||||
* @param member the `QuickOpenItem` object member for comparison.
|
||||
*/
|
||||
private compareItems(
|
||||
a: QuickOpenItem,
|
||||
b: QuickOpenItem,
|
||||
member: 'getLabel' | 'getUri' = 'getLabel'): number {
|
||||
|
||||
private compareItems(a: QuickOpenItem, b: QuickOpenItem, member: 'getLabel' | 'getUri' = 'getLabel'): number {
|
||||
/**
|
||||
* Normalize a given string.
|
||||
*
|
||||
|
@ -500,7 +497,7 @@ export class FileSearchQuickCommandHandler {
|
|||
*/
|
||||
function score(str: string): number {
|
||||
const match = fuzzy.match(query, str);
|
||||
return (match === null) ? 0 : match.score;
|
||||
return match === null ? 0 : match.score;
|
||||
}
|
||||
|
||||
// Some code copied and modified from https://github.com/eclipse-theia/theia/tree/v1.14.0/packages/file-search/src/browser/quick-file-open.ts
|
||||
|
@ -527,16 +524,14 @@ export class FileSearchQuickCommandHandler {
|
|||
|
||||
// If both label scores are identical, perform additional computation.
|
||||
if (scoreA === scoreB) {
|
||||
|
||||
// Favor the label which have the smallest substring index.
|
||||
const indexA: number = itemA.indexOf(query);
|
||||
const indexB: number = itemB.indexOf(query);
|
||||
|
||||
if (indexA === indexB) {
|
||||
|
||||
// Favor the result with the shortest label length.
|
||||
if (itemA.length !== itemB.length) {
|
||||
return (itemA.length < itemB.length) ? -1 : 1;
|
||||
return itemA.length < itemB.length ? -1 : 1;
|
||||
}
|
||||
|
||||
// Fallback to the alphabetical order.
|
||||
|
@ -572,7 +567,6 @@ export class FileSearchQuickCommandHandler {
|
|||
|
||||
@Domain(CommandContribution, KeybindingContribution, QuickOpenContribution)
|
||||
export class FileSearchContribution implements CommandContribution, KeybindingContribution, QuickOpenContribution {
|
||||
|
||||
@Autowired(FileSearchQuickCommandHandler)
|
||||
protected fileSearchQuickCommandHandler: FileSearchQuickCommandHandler;
|
||||
|
||||
|
@ -589,14 +583,10 @@ export class FileSearchContribution implements CommandContribution, KeybindingCo
|
|||
|
||||
registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(quickFileOpen, {
|
||||
execute: () => {
|
||||
return this.quickOpenService.open('...');
|
||||
},
|
||||
execute: () => this.quickOpenService.open('...'),
|
||||
});
|
||||
commands.registerCommand(quickGoToSymbol, {
|
||||
execute: () => {
|
||||
return this.quickOpenService.open('...@');
|
||||
},
|
||||
execute: () => this.quickOpenService.open('...@'),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -606,5 +596,4 @@ export class FileSearchContribution implements CommandContribution, KeybindingCo
|
|||
keybinding: 'ctrlcmd+p',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,5 +27,4 @@ export class ClientAddonModule extends BrowserModule {
|
|||
servicePath: FileDropServicePath,
|
||||
},
|
||||
];
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { IDialogService } from '@opensumi/ide-overlay';
|
|||
|
||||
@Domain(ClientAppContribution)
|
||||
export class LanguageChangeHintContribution implements ClientAppContribution {
|
||||
|
||||
@Autowired(PreferenceService)
|
||||
preferenceService: PreferenceService;
|
||||
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { Autowired } from '@opensumi/di';
|
||||
import { BrowserConnectionCloseEvent, BrowserConnectionOpenEvent, OnEvent, WithEventBus, CommandService, Domain } from '@opensumi/ide-core-common';
|
||||
import {
|
||||
BrowserConnectionCloseEvent,
|
||||
BrowserConnectionOpenEvent,
|
||||
OnEvent,
|
||||
WithEventBus,
|
||||
CommandService,
|
||||
Domain,
|
||||
} from '@opensumi/ide-core-common';
|
||||
import { ClientAppContribution } from '@opensumi/ide-core-browser';
|
||||
|
||||
@Domain(ClientAppContribution)
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
|
||||
@import "~@opensumi/ide-components/lib/style/mixins.less";
|
||||
@import '~@opensumi/ide-components/lib/style/mixins.less';
|
||||
|
||||
.toolbar-customize-overlay {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left:0;
|
||||
top:0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
.toolbar-customize {
|
||||
position: fixed;
|
||||
|
@ -37,7 +36,7 @@
|
|||
align-items: center;
|
||||
> div:first-child {
|
||||
margin-right: 10px;
|
||||
font-size:12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.button-display-select {
|
||||
width: 200px;
|
||||
|
@ -50,5 +49,4 @@
|
|||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { Domain, CommandContribution, CommandRegistry, ComponentContribution, ComponentRegistry, AppConfig, SlotLocation, localize } from '@opensumi/ide-core-browser';
|
||||
import {
|
||||
Domain,
|
||||
CommandContribution,
|
||||
CommandRegistry,
|
||||
ComponentContribution,
|
||||
ComponentRegistry,
|
||||
AppConfig,
|
||||
SlotLocation,
|
||||
localize,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { ToolbarCustomizeComponent, ToolbarCustomizeViewService } from './toolbar-customize';
|
||||
import { MenuContribution, IMenuRegistry, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
|
||||
|
||||
@Domain(CommandContribution, ComponentContribution, MenuContribution)
|
||||
export class ToolbarCustomizeContribution implements CommandContribution, ComponentContribution, MenuContribution {
|
||||
|
||||
@Autowired(AppConfig)
|
||||
config: AppConfig;
|
||||
|
||||
|
@ -13,14 +21,17 @@ export class ToolbarCustomizeContribution implements CommandContribution, Compon
|
|||
viewService: ToolbarCustomizeViewService;
|
||||
|
||||
registerCommands(registry: CommandRegistry) {
|
||||
registry.registerCommand({
|
||||
id: 'toolbar.showCustomizePanel',
|
||||
label: 'Show Toolbar Customization',
|
||||
}, {
|
||||
execute: () => {
|
||||
this.viewService.setVisible(true);
|
||||
registry.registerCommand(
|
||||
{
|
||||
id: 'toolbar.showCustomizePanel',
|
||||
label: 'Show Toolbar Customization',
|
||||
},
|
||||
});
|
||||
{
|
||||
execute: () => {
|
||||
this.viewService.setVisible(true);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
registerComponent(registry: ComponentRegistry) {
|
||||
|
@ -44,5 +55,4 @@ export class ToolbarCustomizeContribution implements CommandContribution, Compon
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Injectable, Autowired } from '@opensumi/di';
|
||||
import { useInjectable, PreferenceService, PreferenceScope, IToolbarRegistry, localize } from '@opensumi/ide-core-browser';
|
||||
import {
|
||||
useInjectable,
|
||||
PreferenceService,
|
||||
PreferenceScope,
|
||||
IToolbarRegistry,
|
||||
localize,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import styles from './style.module.less';
|
||||
import { CheckBox, Select, Button } from '@opensumi/ide-components';
|
||||
|
||||
@Injectable()
|
||||
export class ToolbarCustomizeViewService {
|
||||
|
||||
private _setVisible: (visible: boolean) => void;
|
||||
|
||||
@Autowired(PreferenceService)
|
||||
|
@ -21,7 +26,7 @@ export class ToolbarCustomizeViewService {
|
|||
}
|
||||
|
||||
async toggleActionVisibility(location: string, actionId: string, visible: boolean) {
|
||||
const prev: {[key: string]: string[]} = this.preferenceService.get('toolbar.ignoreActions') || {};
|
||||
const prev: { [key: string]: string[] } = this.preferenceService.get('toolbar.ignoreActions') || {};
|
||||
if (!prev[location]) {
|
||||
prev[location] = [];
|
||||
}
|
||||
|
@ -35,10 +40,11 @@ export class ToolbarCustomizeViewService {
|
|||
prev[location].splice(index, 1);
|
||||
}
|
||||
}
|
||||
const effectingScope = this.preferenceService.inspect('toolbar.ignoreActions')!.workspaceValue ? PreferenceScope.Workspace : PreferenceScope.User;
|
||||
const effectingScope = this.preferenceService.inspect('toolbar.ignoreActions')!.workspaceValue
|
||||
? PreferenceScope.Workspace
|
||||
: PreferenceScope.User;
|
||||
await this.preferenceService.set('toolbar.ignoreActions', prev, effectingScope);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ToolbarCustomizeComponent = () => {
|
||||
|
@ -55,17 +61,17 @@ export const ToolbarCustomizeComponent = () => {
|
|||
|
||||
const locations = registry.getAllLocations();
|
||||
|
||||
const currentPref: {[key: string]: string[]} = preferenceService.get('toolbar.ignoreActions') || {};
|
||||
const currentPref: { [key: string]: string[] } = preferenceService.get('toolbar.ignoreActions') || {};
|
||||
|
||||
let currentDisplayPref: string = preferenceService.get<string>('toolbar.buttonDisplay', 'iconAndText')!;
|
||||
|
||||
function renderLocationPref(location: string) {
|
||||
const groups = [{id: '_head'}, ...(registry.getActionGroups(location) || []), {id: '_tail'}];
|
||||
const groups = [{ id: '_head' }, ...(registry.getActionGroups(location) || []), { id: '_tail' }];
|
||||
const result: React.ReactNode[] = [];
|
||||
const pref = currentPref[location] || [];
|
||||
|
||||
groups.forEach((group, gi) => {
|
||||
const actions = registry.getToolbarActions({location, group: group.id});
|
||||
const actions = registry.getToolbarActions({ location, group: group.id });
|
||||
if (actions && actions.actions.length > 0) {
|
||||
if (result.length > 0) {
|
||||
result.push(<div className={styles['group-split']} key={'split-' + gi}></div>);
|
||||
|
@ -73,12 +79,19 @@ export const ToolbarCustomizeComponent = () => {
|
|||
actions.actions.forEach((action, i) => {
|
||||
let visible = pref.indexOf(action.id) === -1;
|
||||
const id = 'action-toggle-' + action.id;
|
||||
result.push(<div className={styles['action-item']} key={i + '_' + action.id}>
|
||||
<CheckBox onChange={() => {
|
||||
service.toggleActionVisibility(location, action.id, !visible);
|
||||
visible = !visible;
|
||||
}} defaultChecked={visible} id={id} label={action.description}/>
|
||||
</div>);
|
||||
result.push(
|
||||
<div className={styles['action-item']} key={i + '_' + action.id}>
|
||||
<CheckBox
|
||||
onChange={() => {
|
||||
service.toggleActionVisibility(location, action.id, !visible);
|
||||
visible = !visible;
|
||||
}}
|
||||
defaultChecked={visible}
|
||||
id={id}
|
||||
label={action.description}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -86,37 +99,47 @@ export const ToolbarCustomizeComponent = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
return <div key={location} className={styles['toolbar-customize-location']}>
|
||||
{result}
|
||||
</div>;
|
||||
return (
|
||||
<div key={location} className={styles['toolbar-customize-location']}>
|
||||
{result}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={styles['toolbar-customize-overlay']}>
|
||||
<div className={styles['toolbar-customize']}>
|
||||
{
|
||||
locations.map((location) => {
|
||||
return renderLocationPref(location);
|
||||
})
|
||||
}
|
||||
<div className={styles['button-display']}>
|
||||
<div>{localize('toolbar-customize.buttonDisplay.description')}</div>
|
||||
<Select options={[
|
||||
{
|
||||
label: localize('toolbar-customize.buttonDisplay.icon'),
|
||||
value: 'icon',
|
||||
}, {
|
||||
label: localize('toolbar-customize.buttonDisplay.iconAndText'),
|
||||
value: 'iconAndText',
|
||||
},
|
||||
]} value={currentDisplayPref} onChange={(v) => {
|
||||
const effectingScope = preferenceService.inspect('toolbar.buttonDisplay')!.workspaceValue ? PreferenceScope.Workspace : PreferenceScope.User;
|
||||
preferenceService.set('toolbar.buttonDisplay', v, effectingScope);
|
||||
currentDisplayPref = v;
|
||||
}} className={styles['button-display-select']}></Select>
|
||||
</div>
|
||||
<div className={styles['customize-complete']}>
|
||||
<Button type='primary' onClick={() => setVisible(false)}>{localize('toolbar-customize.complete')}</Button>
|
||||
return (
|
||||
<div className={styles['toolbar-customize-overlay']}>
|
||||
<div className={styles['toolbar-customize']}>
|
||||
{locations.map((location) => renderLocationPref(location))}
|
||||
<div className={styles['button-display']}>
|
||||
<div>{localize('toolbar-customize.buttonDisplay.description')}</div>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
label: localize('toolbar-customize.buttonDisplay.icon'),
|
||||
value: 'icon',
|
||||
},
|
||||
{
|
||||
label: localize('toolbar-customize.buttonDisplay.iconAndText'),
|
||||
value: 'iconAndText',
|
||||
},
|
||||
]}
|
||||
value={currentDisplayPref}
|
||||
onChange={(v) => {
|
||||
const effectingScope = preferenceService.inspect('toolbar.buttonDisplay')!.workspaceValue
|
||||
? PreferenceScope.Workspace
|
||||
: PreferenceScope.User;
|
||||
preferenceService.set('toolbar.buttonDisplay', v, effectingScope);
|
||||
currentDisplayPref = v;
|
||||
}}
|
||||
className={styles['button-display-select']}
|
||||
></Select>
|
||||
</div>
|
||||
<div className={styles['customize-complete']}>
|
||||
<Button type='primary' onClick={() => setVisible(false)}>
|
||||
{localize('toolbar-customize.complete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,13 +15,19 @@ describe('comment service test', () => {
|
|||
let commentsService: ICommentsService;
|
||||
beforeAll(() => {
|
||||
(global as any).monaco = createMockedMonaco() as any;
|
||||
injector = createBrowserInjector([ CommentsModule ], new Injector([{
|
||||
token: IContextKeyService,
|
||||
useClass: MockContextKeyService,
|
||||
}, {
|
||||
token: IIconService,
|
||||
useClass: IconService,
|
||||
}]));
|
||||
injector = createBrowserInjector(
|
||||
[CommentsModule],
|
||||
new Injector([
|
||||
{
|
||||
token: IContextKeyService,
|
||||
useClass: MockContextKeyService,
|
||||
},
|
||||
{
|
||||
token: IIconService,
|
||||
useClass: IconService,
|
||||
},
|
||||
]),
|
||||
);
|
||||
commentsService = injector.get<ICommentsService>(ICommentsService);
|
||||
});
|
||||
|
||||
|
@ -36,13 +42,15 @@ describe('comment service test', () => {
|
|||
it('basic props', () => {
|
||||
const uri = URI.file('/test');
|
||||
const thread = commentsService.createThread(uri, positionToRange(1), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}],
|
||||
],
|
||||
});
|
||||
expect(thread.uri.isEqual(uri));
|
||||
expect(thread.range.startLineNumber).toBe(1);
|
||||
|
@ -51,16 +59,18 @@ describe('comment service test', () => {
|
|||
it('thread and comment data', () => {
|
||||
const uri = URI.file('/test');
|
||||
const thread = commentsService.createThread(uri, positionToRange(1), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
data: {
|
||||
b: 1,
|
||||
},
|
||||
},
|
||||
body: '评论内容1',
|
||||
data: {
|
||||
b: 1,
|
||||
},
|
||||
}],
|
||||
],
|
||||
data: {
|
||||
a: 1,
|
||||
},
|
||||
|
@ -72,19 +82,22 @@ describe('comment service test', () => {
|
|||
it('thread add comment', () => {
|
||||
const uri = URI.file('/test');
|
||||
const thread = commentsService.createThread(uri, positionToRange(1));
|
||||
thread.addComment({
|
||||
mode: CommentMode.Preview,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
thread.addComment(
|
||||
{
|
||||
mode: CommentMode.Preview,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}, {
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容2',
|
||||
},
|
||||
body: '评论内容2',
|
||||
});
|
||||
);
|
||||
expect(thread.comments.length).toBe(2);
|
||||
expect(thread.comments[1].mode).toBe(CommentMode.Editor);
|
||||
});
|
||||
|
@ -92,19 +105,22 @@ describe('comment service test', () => {
|
|||
it('thread dispose', () => {
|
||||
const uri = URI.file('/test');
|
||||
const thread = commentsService.createThread(uri, positionToRange(1));
|
||||
thread.addComment({
|
||||
mode: CommentMode.Preview,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
thread.addComment(
|
||||
{
|
||||
mode: CommentMode.Preview,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}, {
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容2',
|
||||
},
|
||||
body: '评论内容2',
|
||||
});
|
||||
);
|
||||
thread.dispose();
|
||||
expect(thread.comments.length).toBe(0);
|
||||
});
|
||||
|
|
|
@ -30,27 +30,35 @@ describe('comment service test', () => {
|
|||
}),
|
||||
});
|
||||
currentEditor = mockService({ monacoEditor });
|
||||
injector = createBrowserInjector([ CommentsModule ], new Injector([{
|
||||
token: IContextKeyService,
|
||||
useClass: MockContextKeyService,
|
||||
}, {
|
||||
token: IIconService,
|
||||
useClass: IconService,
|
||||
}, {
|
||||
token: ResourceService,
|
||||
useClass: ResourceServiceImpl,
|
||||
}, {
|
||||
token: EditorCollectionService,
|
||||
useValue: mockService({
|
||||
listEditors: () => [currentEditor],
|
||||
}),
|
||||
}, {
|
||||
token: IEditorDecorationCollectionService,
|
||||
useValue: mockService({
|
||||
registerDecorationProvider: () => Disposable.NULL,
|
||||
}),
|
||||
}]));
|
||||
|
||||
injector = createBrowserInjector(
|
||||
[CommentsModule],
|
||||
new Injector([
|
||||
{
|
||||
token: IContextKeyService,
|
||||
useClass: MockContextKeyService,
|
||||
},
|
||||
{
|
||||
token: IIconService,
|
||||
useClass: IconService,
|
||||
},
|
||||
{
|
||||
token: ResourceService,
|
||||
useClass: ResourceServiceImpl,
|
||||
},
|
||||
{
|
||||
token: EditorCollectionService,
|
||||
useValue: mockService({
|
||||
listEditors: () => [currentEditor],
|
||||
}),
|
||||
},
|
||||
{
|
||||
token: IEditorDecorationCollectionService,
|
||||
useValue: mockService({
|
||||
registerDecorationProvider: () => Disposable.NULL,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -68,7 +76,7 @@ describe('comment service test', () => {
|
|||
|
||||
it('create thread', () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread ] = createTestThreads(uri);
|
||||
const [thread] = createTestThreads(uri);
|
||||
expect(thread.uri.isEqual(uri));
|
||||
expect(thread.range.startLineNumber).toBe(1);
|
||||
expect(thread.comments[0].body).toBe('评论内容1');
|
||||
|
@ -76,7 +84,7 @@ describe('comment service test', () => {
|
|||
|
||||
it('get commentsThreads', () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread, thread2 ] = createTestThreads(uri);
|
||||
const [thread, thread2] = createTestThreads(uri);
|
||||
expect(commentsService.commentsThreads.length).toBe(2);
|
||||
// 按照创建时间排列
|
||||
expect(commentsService.commentsThreads[0].id).toBe(thread.id);
|
||||
|
@ -85,7 +93,7 @@ describe('comment service test', () => {
|
|||
|
||||
it('getThreadByUri', () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread, thread2 ] = createTestThreads(uri);
|
||||
const [thread, thread2] = createTestThreads(uri);
|
||||
const threads = commentsService.getThreadsByUri(uri);
|
||||
expect(threads.length).toBe(2);
|
||||
// 按照 range 升序排列
|
||||
|
@ -95,7 +103,7 @@ describe('comment service test', () => {
|
|||
|
||||
it('commentsTreeNodes', () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread, thread2 ] = createTestThreads(uri);
|
||||
const [thread, thread2] = createTestThreads(uri);
|
||||
thread.addComment({
|
||||
mode: CommentMode.Preview,
|
||||
author: {
|
||||
|
@ -135,13 +143,15 @@ describe('comment service test', () => {
|
|||
commentsService.onThreadsCreated(threadsCreatedListener);
|
||||
const uri = URI.file('/test');
|
||||
const thread = commentsService.createThread(uri, positionToRange(1), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}],
|
||||
],
|
||||
});
|
||||
expect(threadsCreatedListener.mock.calls.length).toBe(1);
|
||||
expect(threadsCreatedListener.mock.calls[0][0].id).toBe(thread.id);
|
||||
|
@ -152,20 +162,22 @@ describe('comment service test', () => {
|
|||
commentsService.onThreadsChanged(threadsChangedListener);
|
||||
const uri = URI.file('/test');
|
||||
commentsService.createThread(uri, positionToRange(1), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}],
|
||||
],
|
||||
});
|
||||
expect(threadsChangedListener.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('调用 showWidgetsIfShowed 时已经被隐藏的 widget 不会被调用 show 方法', async () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread ] = createTestThreads(uri);
|
||||
const [thread] = createTestThreads(uri);
|
||||
currentEditor.currentUri = uri;
|
||||
// 生成一个 widget
|
||||
thread.show(currentEditor);
|
||||
|
@ -183,7 +195,7 @@ describe('comment service test', () => {
|
|||
|
||||
it('如果 isShow 为 true 才会调用 show 方法', async () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread ] = createTestThreads(uri);
|
||||
const [thread] = createTestThreads(uri);
|
||||
currentEditor.currentUri = uri;
|
||||
// 生成一个 widget
|
||||
thread.show(currentEditor);
|
||||
|
@ -199,7 +211,7 @@ describe('comment service test', () => {
|
|||
|
||||
it('通过 dispose 的方式隐藏 widget,不会影响 isShow', async () => {
|
||||
const uri = URI.file('/test');
|
||||
const [ thread ] = createTestThreads(uri);
|
||||
const [thread] = createTestThreads(uri);
|
||||
currentEditor.currentUri = uri;
|
||||
// 生成一个 widget
|
||||
thread.show(currentEditor);
|
||||
|
@ -224,22 +236,26 @@ describe('comment service test', () => {
|
|||
function createTestThreads(uri: URI) {
|
||||
return [
|
||||
commentsService.createThread(uri, positionToRange(1), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}],
|
||||
],
|
||||
}),
|
||||
commentsService.createThread(uri, positionToRange(2), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容2',
|
||||
},
|
||||
body: '评论内容2',
|
||||
}],
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -16,13 +16,19 @@ describe('comment service test', () => {
|
|||
let commentsFeatureRegistry: ICommentsFeatureRegistry;
|
||||
beforeAll(() => {
|
||||
(global as any).monaco = createMockedMonaco() as any;
|
||||
injector = createBrowserInjector([ CommentsModule ], new Injector([{
|
||||
token: IContextKeyService,
|
||||
useClass: MockContextKeyService,
|
||||
}, {
|
||||
token: IIconService,
|
||||
useClass: IconService,
|
||||
}]));
|
||||
injector = createBrowserInjector(
|
||||
[CommentsModule],
|
||||
new Injector([
|
||||
{
|
||||
token: IContextKeyService,
|
||||
useClass: MockContextKeyService,
|
||||
},
|
||||
{
|
||||
token: IIconService,
|
||||
useClass: IconService,
|
||||
},
|
||||
]),
|
||||
);
|
||||
commentsService = injector.get<ICommentsService>(ICommentsService);
|
||||
commentsFeatureRegistry = injector.get<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
});
|
||||
|
@ -63,21 +69,23 @@ describe('comment service test', () => {
|
|||
|
||||
it('registerPanelTreeNodeHandler', () => {
|
||||
// 先绑定 node 节点处理函数
|
||||
commentsFeatureRegistry.registerPanelTreeNodeHandler((nodes) => {
|
||||
return nodes.map((node) => {
|
||||
commentsFeatureRegistry.registerPanelTreeNodeHandler((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.name = '111';
|
||||
return node;
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
const uri = URI.file('/test');
|
||||
commentsService.createThread(uri, positionToRange(1), {
|
||||
comments: [{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
comments: [
|
||||
{
|
||||
mode: CommentMode.Editor,
|
||||
author: {
|
||||
name: '蛋总',
|
||||
},
|
||||
body: '评论内容1',
|
||||
},
|
||||
body: '评论内容1',
|
||||
}],
|
||||
],
|
||||
});
|
||||
const nodes = commentsService.commentsTreeNodes;
|
||||
// name 不会是 test,而是被 handler 处理过的 111
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { IThreadComment, ICommentsThread, CommentReaction, CommentReactionClick, SwitchCommandReaction } from '../common';
|
||||
import {
|
||||
IThreadComment,
|
||||
ICommentsThread,
|
||||
CommentReaction,
|
||||
CommentReactionClick,
|
||||
SwitchCommandReaction,
|
||||
} from '../common';
|
||||
import { Button } from '@opensumi/ide-core-browser/lib/components';
|
||||
import { useInjectable, IEventBus, getExternalIcon, Disposable } from '@opensumi/ide-core-browser';
|
||||
import { IIconService, IconType } from '@opensumi/ide-theme';
|
||||
|
@ -22,25 +28,34 @@ export const CommentReactionSwitcher: React.FC<{
|
|||
const disposer = new Disposable();
|
||||
const subMenuId = `comment_reaction_switcher_submenu_${key}`;
|
||||
|
||||
disposer.addDispose(menuRegistry.registerMenuItem(menuId, {
|
||||
submenu: subMenuId,
|
||||
// 目前 label 必须要填
|
||||
label: subMenuId,
|
||||
iconClass: getExternalIcon('reactions'),
|
||||
group: 'navigation',
|
||||
}));
|
||||
disposer.addDispose(
|
||||
menuRegistry.registerMenuItem(menuId, {
|
||||
submenu: subMenuId,
|
||||
// 目前 label 必须要填
|
||||
label: subMenuId,
|
||||
iconClass: getExternalIcon('reactions'),
|
||||
group: 'navigation',
|
||||
}),
|
||||
);
|
||||
|
||||
disposer.addDispose(menuRegistry.registerMenuItems(subMenuId, comment.reactions!.map((reaction) => ({
|
||||
command: {
|
||||
id: SwitchCommandReaction,
|
||||
label: reaction.label!,
|
||||
},
|
||||
extraTailArgs: [{
|
||||
thread,
|
||||
comment,
|
||||
reaction,
|
||||
}],
|
||||
}))));
|
||||
disposer.addDispose(
|
||||
menuRegistry.registerMenuItems(
|
||||
subMenuId,
|
||||
comment.reactions!.map((reaction) => ({
|
||||
command: {
|
||||
id: SwitchCommandReaction,
|
||||
label: reaction.label!,
|
||||
},
|
||||
extraTailArgs: [
|
||||
{
|
||||
thread,
|
||||
comment,
|
||||
reaction,
|
||||
},
|
||||
],
|
||||
})),
|
||||
),
|
||||
);
|
||||
return () => disposer.dispose();
|
||||
}, []);
|
||||
|
||||
|
@ -49,44 +64,42 @@ export const CommentReactionSwitcher: React.FC<{
|
|||
return menu;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InlineActionBar
|
||||
className={className}
|
||||
menus={reactionsContext}
|
||||
regroup={(nav) => [nav, []]}
|
||||
type='icon'
|
||||
/>
|
||||
);
|
||||
return <InlineActionBar className={className} menus={reactionsContext} regroup={(nav) => [nav, []]} type='icon' />;
|
||||
});
|
||||
|
||||
export const CommentReactions: React.FC<{
|
||||
thread: ICommentsThread;
|
||||
comment: IThreadComment,
|
||||
comment: IThreadComment;
|
||||
}> = observer(({ thread, comment }) => {
|
||||
const eventBus = useInjectable<IEventBus>(IEventBus);
|
||||
const iconService = useInjectable<IIconService>(IIconService);
|
||||
const handleClickReaction = React.useCallback((reaction: CommentReaction) => {
|
||||
eventBus.fire(new CommentReactionClick({
|
||||
thread,
|
||||
comment,
|
||||
reaction,
|
||||
}));
|
||||
eventBus.fire(
|
||||
new CommentReactionClick({
|
||||
thread,
|
||||
comment,
|
||||
reaction,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.comment_reactions}>
|
||||
{comment.reactions?.filter((reaction) => reaction.count !== 0)
|
||||
{comment.reactions
|
||||
?.filter((reaction) => reaction.count !== 0)
|
||||
.map((reaction) => (
|
||||
<Button
|
||||
key={reaction.label}
|
||||
onClick={() => handleClickReaction(reaction)}
|
||||
type='secondary'
|
||||
size='small'
|
||||
title={reaction.label}
|
||||
className={styles.comment_reaction}
|
||||
iconClass={iconService.fromIcon('', reaction.iconPath.toString(), IconType.Background)}
|
||||
> {reaction.count}</Button>
|
||||
))}
|
||||
<Button
|
||||
key={reaction.label}
|
||||
onClick={() => handleClickReaction(reaction)}
|
||||
type='secondary'
|
||||
size='small'
|
||||
title={reaction.label}
|
||||
className={styles.comment_reaction}
|
||||
iconClass={iconService.fromIcon('', reaction.iconPath.toString(), IconType.Background)}
|
||||
>
|
||||
{reaction.count}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,9 +8,7 @@ const ShadowContent = ({ root, children }) => ReactDOM.createPortal(children, ro
|
|||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.link = ( href, title, text ) => {
|
||||
return `<a target="_blank" rel="noopener" href="${href}" title="${title}">${text}</a>`;
|
||||
};
|
||||
renderer.link = (href, title, text) => `<a target="_blank" rel="noopener" href="${href}" title="${title}">${text}</a>`;
|
||||
|
||||
export const CommentsBody: React.FC<{
|
||||
body: string;
|
||||
|
@ -31,21 +29,22 @@ export const CommentsBody: React.FC<{
|
|||
{shadowRoot && (
|
||||
<ShadowContent root={shadowRoot}>
|
||||
<style>{markdownCss}</style>
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: marked(body, {
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false,
|
||||
renderer,
|
||||
}),
|
||||
}}></div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(body, {
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false,
|
||||
renderer,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
</ShadowContent>
|
||||
)}
|
||||
</div>
|
||||
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import { Injectable } from '@opensumi/di';
|
||||
import { CommentsPanelOptions, ICommentsFeatureRegistry, PanelTreeNodeHandler, FileUploadHandler, MentionsOptions, ZoneWidgerRender, ICommentsConfig, ICommentProviderFeature } from '../common';
|
||||
import {
|
||||
CommentsPanelOptions,
|
||||
ICommentsFeatureRegistry,
|
||||
PanelTreeNodeHandler,
|
||||
FileUploadHandler,
|
||||
MentionsOptions,
|
||||
ZoneWidgerRender,
|
||||
ICommentsConfig,
|
||||
ICommentProviderFeature,
|
||||
} from '../common';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsFeatureRegistry implements ICommentsFeatureRegistry {
|
||||
|
||||
private config: ICommentsConfig = {};
|
||||
|
||||
private options: CommentsPanelOptions = {};
|
||||
|
@ -32,7 +40,7 @@ export class CommentsFeatureRegistry implements ICommentsFeatureRegistry {
|
|||
registerPanelOptions(options: CommentsPanelOptions): void {
|
||||
this.options = {
|
||||
...this.options,
|
||||
... options,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import React from 'react';
|
||||
import styles from './comments.module.less';
|
||||
import { IThreadComment, ICommentsCommentTitle, CommentMode, ICommentReply, ICommentsCommentContext, ICommentsZoneWidget, ICommentsFeatureRegistry, ICommentsThread } from '../common';
|
||||
import {
|
||||
IThreadComment,
|
||||
ICommentsCommentTitle,
|
||||
CommentMode,
|
||||
ICommentReply,
|
||||
ICommentsCommentContext,
|
||||
ICommentsZoneWidget,
|
||||
ICommentsFeatureRegistry,
|
||||
ICommentsThread,
|
||||
} from '../common';
|
||||
import { InlineActionBar } from '@opensumi/ide-core-browser/lib/components/actions';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { CommentsTextArea } from './comments-textarea.view';
|
||||
|
@ -11,12 +20,20 @@ import { CommentsBody } from './comments-body';
|
|||
import marked from 'marked';
|
||||
import { CommentReactions, CommentReactionSwitcher } from './comment-reactions.view';
|
||||
|
||||
const useCommentContext
|
||||
= (contextKeyService: IContextKeyService, comment: IThreadComment)
|
||||
: [string, React.Dispatch<React.SetStateAction<string>>, (event: React.ChangeEvent<HTMLTextAreaElement>) => void, IMenu, IMenu, (files: FileList) => Promise<void>] => {
|
||||
const useCommentContext = (
|
||||
contextKeyService: IContextKeyService,
|
||||
comment: IThreadComment,
|
||||
): [
|
||||
string,
|
||||
React.Dispatch<React.SetStateAction<string>>,
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>) => void,
|
||||
IMenu,
|
||||
IMenu,
|
||||
(files: FileList) => Promise<void>,
|
||||
] => {
|
||||
const menuService = useInjectable<AbstractMenuService>(AbstractMenuService);
|
||||
const { body, contextValue } = comment;
|
||||
const [ textValue, setTextValue ] = React.useState('');
|
||||
const [textValue, setTextValue] = React.useState('');
|
||||
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
|
||||
// set textValue when body changed
|
||||
|
@ -25,27 +42,22 @@ const useCommentContext
|
|||
}, [body]);
|
||||
|
||||
// Each comment has its own commentContext and commentTitleContext.
|
||||
const commentContextService = React.useMemo(() => {
|
||||
return contextKeyService.createScoped();
|
||||
}, []);
|
||||
const commentContextService = React.useMemo(() => contextKeyService.createScoped(), []);
|
||||
// it's value will true when textarea is empty
|
||||
const commentIsEmptyContext = React.useMemo(() => {
|
||||
return commentContextService.createKey<boolean>('commentIsEmpty', !comment.body);
|
||||
}, []);
|
||||
const commentIsEmptyContext = React.useMemo(
|
||||
() => commentContextService.createKey<boolean>('commentIsEmpty', !comment.body),
|
||||
[],
|
||||
);
|
||||
// below the comment textarea
|
||||
const commentContext = React.useMemo(() => {
|
||||
return menuService.createMenu(
|
||||
MenuId.CommentsCommentContext,
|
||||
commentContextService,
|
||||
);
|
||||
}, []);
|
||||
const commentContext = React.useMemo(
|
||||
() => menuService.createMenu(MenuId.CommentsCommentContext, commentContextService),
|
||||
[],
|
||||
);
|
||||
// after the comment body
|
||||
const commentTitleContext = React.useMemo(() => {
|
||||
return menuService.createMenu(
|
||||
MenuId.CommentsCommentTitle,
|
||||
commentContextService,
|
||||
);
|
||||
}, []);
|
||||
const commentTitleContext = React.useMemo(
|
||||
() => menuService.createMenu(MenuId.CommentsCommentTitle, commentContextService),
|
||||
[],
|
||||
);
|
||||
|
||||
const itemCommentContext = React.useRef(commentContextService.createKey('comment', contextValue));
|
||||
|
||||
|
@ -58,42 +70,32 @@ const useCommentContext
|
|||
setTextValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleDragFiles = React.useCallback(async (files: FileList) => {
|
||||
if (fileUploadHandler) {
|
||||
const appendText = await fileUploadHandler(textValue, files);
|
||||
setTextValue((text) => {
|
||||
const value = text + appendText;
|
||||
commentIsEmptyContext.set(!value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}, [ textValue ]);
|
||||
const handleDragFiles = React.useCallback(
|
||||
async (files: FileList) => {
|
||||
if (fileUploadHandler) {
|
||||
const appendText = await fileUploadHandler(textValue, files);
|
||||
setTextValue((text) => {
|
||||
const value = text + appendText;
|
||||
commentIsEmptyContext.set(!value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
},
|
||||
[textValue],
|
||||
);
|
||||
|
||||
return [
|
||||
textValue,
|
||||
setTextValue,
|
||||
onChangeTextArea,
|
||||
commentContext,
|
||||
commentTitleContext,
|
||||
handleDragFiles,
|
||||
];
|
||||
return [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles];
|
||||
};
|
||||
|
||||
const ReplyItem: React.FC<{
|
||||
reply: IThreadComment,
|
||||
thread: ICommentsThread,
|
||||
reply: IThreadComment;
|
||||
thread: ICommentsThread;
|
||||
}> = observer(({ reply, thread }) => {
|
||||
const { contextKeyService } = thread;
|
||||
const { author, label, body, mode } = reply;
|
||||
const iconUrl = author.iconPath?.toString();
|
||||
const [
|
||||
textValue,
|
||||
setTextValue,
|
||||
onChangeTextArea,
|
||||
commentContext,
|
||||
commentTitleContext,
|
||||
handleDragFiles,
|
||||
] = useCommentContext(contextKeyService, reply);
|
||||
const [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles] =
|
||||
useCommentContext(contextKeyService, reply);
|
||||
|
||||
// 判断是正常 Inline Text 还是 Markdown Text
|
||||
const isInlineText = React.useMemo(() => {
|
||||
|
@ -107,60 +109,38 @@ const ReplyItem: React.FC<{
|
|||
<div className={styles.reply_item}>
|
||||
{isUndefined(mode) || mode === CommentMode.Preview ? (
|
||||
<div>
|
||||
{ isInlineText ? (
|
||||
{isInlineText ? (
|
||||
<>
|
||||
{iconUrl && (
|
||||
<img
|
||||
className={styles.reply_item_icon}
|
||||
src={iconUrl}
|
||||
alt={author.name}
|
||||
{iconUrl && <img className={styles.reply_item_icon} src={iconUrl} alt={author.name} />}
|
||||
<span className={styles.comment_item_author_name}>{author.name}</span>
|
||||
{typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
|
||||
{' : '}
|
||||
<span className={styles.comment_item_body}>{body}</span>
|
||||
{reply.reactions && reply.reactions.length > 0 && (
|
||||
<CommentReactionSwitcher className={styles.reply_item_title} thread={thread} comment={reply} />
|
||||
)}
|
||||
<InlineActionBar<ICommentsCommentTitle>
|
||||
separator='inline'
|
||||
className={styles.reply_item_title}
|
||||
menus={commentTitleContext}
|
||||
context={[
|
||||
{
|
||||
thread,
|
||||
comment: reply,
|
||||
menuId: MenuId.CommentsCommentTitle,
|
||||
},
|
||||
]}
|
||||
type='icon'
|
||||
/>
|
||||
)}
|
||||
<span className={styles.comment_item_author_name}>
|
||||
{author.name}
|
||||
</span>
|
||||
{typeof label === 'string' ? (
|
||||
<span className={styles.comment_item_label}>{label}</span>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
{ ' : ' }
|
||||
<span className={styles.comment_item_body}>{body}</span>
|
||||
{(reply.reactions && reply.reactions.length > 0) && <CommentReactionSwitcher className={styles.reply_item_title} thread={thread} comment={reply} />}
|
||||
<InlineActionBar<ICommentsCommentTitle>
|
||||
separator='inline'
|
||||
className={styles.reply_item_title}
|
||||
menus={commentTitleContext}
|
||||
context={[
|
||||
{
|
||||
thread,
|
||||
comment: reply,
|
||||
menuId: MenuId.CommentsCommentTitle,
|
||||
},
|
||||
]}
|
||||
type='icon'
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.comment_item_markdown_header}>
|
||||
<div>
|
||||
{iconUrl && (
|
||||
<img
|
||||
className={styles.reply_item_icon}
|
||||
src={iconUrl}
|
||||
alt={author.name}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.comment_item_author_name}>
|
||||
{author.name}
|
||||
</span>
|
||||
{typeof label === 'string' ? (
|
||||
<span className={styles.comment_item_label}>{label}</span>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
{ ' : ' }
|
||||
{iconUrl && <img className={styles.reply_item_icon} src={iconUrl} alt={author.name} />}
|
||||
<span className={styles.comment_item_author_name}>{author.name}</span>
|
||||
{typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
|
||||
{' : '}
|
||||
</div>
|
||||
<InlineActionBar<ICommentsCommentTitle>
|
||||
separator='inline'
|
||||
|
@ -208,35 +188,27 @@ const ReplyItem: React.FC<{
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{(reply.reactions && reply.reactions.length > 0) && <CommentReactions thread={thread} comment={reply} /> }
|
||||
{reply.reactions && reply.reactions.length > 0 && <CommentReactions thread={thread} comment={reply} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const CommentItem: React.FC<{
|
||||
thread: ICommentsThread,
|
||||
commentThreadContext: IMenu,
|
||||
widget: ICommentsZoneWidget,
|
||||
thread: ICommentsThread;
|
||||
commentThreadContext: IMenu;
|
||||
widget: ICommentsZoneWidget;
|
||||
}> = observer(({ thread, commentThreadContext, widget }) => {
|
||||
const { readOnly, contextKeyService } = thread;
|
||||
const [ showReply, setShowReply ] = React.useState(false);
|
||||
const [ replyText, setReplyText ] = React.useState('');
|
||||
const [ comment, ...replies ] = thread.comments;
|
||||
const [showReply, setShowReply] = React.useState(false);
|
||||
const [replyText, setReplyText] = React.useState('');
|
||||
const [comment, ...replies] = thread.comments;
|
||||
const { author, label, body, mode } = comment;
|
||||
const iconUrl = author.iconPath?.toString();
|
||||
const [
|
||||
textValue,
|
||||
setTextValue,
|
||||
onChangeTextArea,
|
||||
commentContext,
|
||||
commentTitleContext,
|
||||
handleDragFiles,
|
||||
] = useCommentContext(contextKeyService, comment);
|
||||
const [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles] =
|
||||
useCommentContext(contextKeyService, comment);
|
||||
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
|
||||
const replyIsEmptyContext = React.useMemo(() => {
|
||||
return contextKeyService.createKey('commentIsEmpty', true);
|
||||
}, []);
|
||||
const replyIsEmptyContext = React.useMemo(() => contextKeyService.createKey('commentIsEmpty', true), []);
|
||||
|
||||
// modify reply
|
||||
function onChangeReply(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
|
@ -244,63 +216,61 @@ export const CommentItem: React.FC<{
|
|||
setReplyText(event.target.value);
|
||||
}
|
||||
|
||||
const handleDragFilesToReply = React.useCallback(async (files: FileList) => {
|
||||
if (fileUploadHandler) {
|
||||
const appendText = await fileUploadHandler(textValue, files);
|
||||
setReplyText((text) => {
|
||||
const value = text + appendText;
|
||||
replyIsEmptyContext.set(!value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}, [ replyText ]);
|
||||
const handleDragFilesToReply = React.useCallback(
|
||||
async (files: FileList) => {
|
||||
if (fileUploadHandler) {
|
||||
const appendText = await fileUploadHandler(textValue, files);
|
||||
setReplyText((text) => {
|
||||
const value = text + appendText;
|
||||
replyIsEmptyContext.set(!value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
},
|
||||
[replyText],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.comment_item}>
|
||||
{iconUrl && (
|
||||
<img
|
||||
className={styles.comment_item_icon}
|
||||
src={iconUrl}
|
||||
alt={author.name}
|
||||
/>
|
||||
)}
|
||||
{iconUrl && <img className={styles.comment_item_icon} src={iconUrl} alt={author.name} />}
|
||||
<div className={styles.comment_item_content}>
|
||||
<div className={styles.comment_item_head}>
|
||||
<div className={styles.comment_item_name}>
|
||||
<span className={styles.comment_item_author_name}>
|
||||
{author.name}
|
||||
</span>
|
||||
{typeof label === 'string' ? (
|
||||
<span className={styles.comment_item_label}>{label}</span>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
<span className={styles.comment_item_author_name}>{author.name}</span>
|
||||
{typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
|
||||
</div>
|
||||
<div className={styles.comment_item_actions}>
|
||||
{(comment.reactions && comment.reactions.length > 0) && <CommentReactionSwitcher thread={thread} comment={comment} /> }
|
||||
{comment.reactions && comment.reactions.length > 0 && (
|
||||
<CommentReactionSwitcher thread={thread} comment={comment} />
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button className={styles.comment_item_reply_button} size='small' type='secondary' onClick={() => setShowReply(true)}>
|
||||
{localize('comments.thread.action.reply')}
|
||||
<Button
|
||||
className={styles.comment_item_reply_button}
|
||||
size='small'
|
||||
type='secondary'
|
||||
onClick={() => setShowReply(true)}
|
||||
>
|
||||
{localize('comments.thread.action.reply')}
|
||||
</Button>
|
||||
)}
|
||||
<InlineActionBar<ICommentsCommentTitle>
|
||||
menus={commentTitleContext}
|
||||
context={[
|
||||
{
|
||||
thread,
|
||||
comment,
|
||||
menuId: MenuId.CommentsCommentTitle,
|
||||
},
|
||||
]}
|
||||
type='button'
|
||||
/>
|
||||
<InlineActionBar<ICommentsCommentTitle>
|
||||
menus={commentTitleContext}
|
||||
context={[
|
||||
{
|
||||
thread,
|
||||
comment,
|
||||
menuId: MenuId.CommentsCommentTitle,
|
||||
},
|
||||
]}
|
||||
type='button'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isUndefined(mode) || mode === CommentMode.Preview ? (
|
||||
<CommentsBody body={body} />
|
||||
) : (
|
||||
<div>
|
||||
<CommentsTextArea
|
||||
<div>
|
||||
<CommentsTextArea
|
||||
value={textValue}
|
||||
autoFocus={true}
|
||||
onChange={onChangeTextArea}
|
||||
|
@ -326,35 +296,39 @@ export const CommentItem: React.FC<{
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{(comment.reactions && comment.reactions.length > 0) && <CommentReactions thread={thread} comment={comment} /> }
|
||||
{comment.reactions && comment.reactions.length > 0 && <CommentReactions thread={thread} comment={comment} />}
|
||||
{(replies.length > 0 || showReply) && (
|
||||
<div className={styles.comment_item_reply_wrap}>
|
||||
{replies.map((reply) => <ReplyItem key={reply.id} thread={thread} reply={reply} />)}
|
||||
{replies.map((reply) => (
|
||||
<ReplyItem key={reply.id} thread={thread} reply={reply} />
|
||||
))}
|
||||
{showReply && (
|
||||
<div>
|
||||
<CommentsTextArea
|
||||
autoFocus={true}
|
||||
value={replyText}
|
||||
onChange={onChangeReply}
|
||||
placeholder={`${localize('comments.reply.placeholder')}...`}
|
||||
dragFiles={handleDragFilesToReply}
|
||||
/>
|
||||
<InlineActionBar<ICommentReply>
|
||||
className={styles.comment_item_reply}
|
||||
menus={commentThreadContext}
|
||||
context={[{
|
||||
thread,
|
||||
text: replyText,
|
||||
widget,
|
||||
menuId: MenuId.CommentsCommentThreadContext,
|
||||
}]}
|
||||
separator='inline'
|
||||
type='button'
|
||||
afterClick={() => {
|
||||
setReplyText('');
|
||||
setShowReply(false);
|
||||
}}
|
||||
/>
|
||||
autoFocus={true}
|
||||
value={replyText}
|
||||
onChange={onChangeReply}
|
||||
placeholder={`${localize('comments.reply.placeholder')}...`}
|
||||
dragFiles={handleDragFilesToReply}
|
||||
/>
|
||||
<InlineActionBar<ICommentReply>
|
||||
className={styles.comment_item_reply}
|
||||
menus={commentThreadContext}
|
||||
context={[
|
||||
{
|
||||
thread,
|
||||
text: replyText,
|
||||
widget,
|
||||
menuId: MenuId.CommentsCommentThreadContext,
|
||||
},
|
||||
]}
|
||||
separator='inline'
|
||||
type='button'
|
||||
afterClick={() => {
|
||||
setReplyText('');
|
||||
setShowReply(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -7,93 +7,96 @@ import styles from './comments.module.less';
|
|||
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||
import clx from 'classnames';
|
||||
|
||||
export const CommentsPanel = observer<{ viewState: ViewState; className?: string}>((props) => {
|
||||
export const CommentsPanel = observer<{ viewState: ViewState; className?: string }>((props) => {
|
||||
const commentsService = useInjectable<ICommentsService>(ICommentsService);
|
||||
const workbenchEditorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
|
||||
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
const [ treeNodes, setTreeNodes ] = React.useState<ICommentsTreeNode[]>([]);
|
||||
const [treeNodes, setTreeNodes] = React.useState<ICommentsTreeNode[]>([]);
|
||||
const eventBus: IEventBus = useInjectable(IEventBus);
|
||||
|
||||
React.useEffect(() => {
|
||||
eventBus.on(CommentPanelCollapse, () => {
|
||||
setTreeNodes((nodes) => nodes.map((node) => {
|
||||
if (!isUndefined(node.expanded)) {
|
||||
node.expanded = false;
|
||||
}
|
||||
return node;
|
||||
}));
|
||||
setTreeNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (!isUndefined(node.expanded)) {
|
||||
node.expanded = false;
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getRenderTree = React.useCallback((nodes: ICommentsTreeNode[]) => {
|
||||
return nodes.filter((node) => {
|
||||
if (node && node.parent) {
|
||||
if (node.parent.expanded === false || node.parent.parent?.expanded === false) {
|
||||
return false;
|
||||
const getRenderTree = React.useCallback(
|
||||
(nodes: ICommentsTreeNode[]) =>
|
||||
nodes.filter((node) => {
|
||||
if (node && node.parent) {
|
||||
if (node.parent.expanded === false || node.parent.parent?.expanded === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, []);
|
||||
return true;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTreeNodes(commentsService.commentsTreeNodes);
|
||||
}, [commentsService.commentsTreeNodes]);
|
||||
|
||||
const handleSelect = React.useCallback(([item]: [ ICommentsTreeNode ]) => {
|
||||
// 可能点击到空白位置
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const handleSelect = React.useCallback(
|
||||
([item]: [ICommentsTreeNode]) => {
|
||||
// 可能点击到空白位置
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUndefined(item.expanded)) {
|
||||
const newNodes = treeNodes.map((node) => {
|
||||
if (node.id === item.id) {
|
||||
node.expanded = !node.expanded;
|
||||
}
|
||||
node.selected = node.id === item.id;
|
||||
return node;
|
||||
});
|
||||
setTreeNodes(newNodes);
|
||||
} else {
|
||||
const newNodes = treeNodes.map((node) => {
|
||||
node.selected = node.id === item.id;
|
||||
return node;
|
||||
});
|
||||
setTreeNodes(newNodes);
|
||||
}
|
||||
if (!isUndefined(item.expanded)) {
|
||||
const newNodes = treeNodes.map((node) => {
|
||||
if (node.id === item.id) {
|
||||
node.expanded = !node.expanded;
|
||||
}
|
||||
node.selected = node.id === item.id;
|
||||
return node;
|
||||
});
|
||||
setTreeNodes(newNodes);
|
||||
} else {
|
||||
const newNodes = treeNodes.map((node) => {
|
||||
node.selected = node.id === item.id;
|
||||
return node;
|
||||
});
|
||||
setTreeNodes(newNodes);
|
||||
}
|
||||
|
||||
if (item.onSelect) {
|
||||
item.onSelect(item);
|
||||
} else {
|
||||
workbenchEditorService.open(item.uri!, {
|
||||
range: item.thread.range,
|
||||
});
|
||||
}
|
||||
}, [workbenchEditorService, treeNodes]);
|
||||
if (item.onSelect) {
|
||||
item.onSelect(item);
|
||||
} else {
|
||||
workbenchEditorService.open(item.uri!, {
|
||||
range: item.thread.range,
|
||||
});
|
||||
}
|
||||
},
|
||||
[workbenchEditorService, treeNodes],
|
||||
);
|
||||
|
||||
const commentsPanelOptions = React.useMemo(() => {
|
||||
return commentsFeatureRegistry.getCommentsPanelOptions();
|
||||
}, []);
|
||||
const commentsPanelOptions = React.useMemo(() => commentsFeatureRegistry.getCommentsPanelOptions(), []);
|
||||
|
||||
const headerComponent = React.useMemo(() => {
|
||||
return commentsPanelOptions.header;
|
||||
}, [commentsPanelOptions]);
|
||||
const headerComponent = React.useMemo(() => commentsPanelOptions.header, [commentsPanelOptions]);
|
||||
|
||||
const treeHeight = React.useMemo(() => {
|
||||
return props.viewState.height - (headerComponent?.height || 0);
|
||||
}, [props.viewState.height]);
|
||||
const treeHeight = React.useMemo(
|
||||
() => props.viewState.height - (headerComponent?.height || 0),
|
||||
[props.viewState.height],
|
||||
);
|
||||
|
||||
const scrollContainerStyle = React.useMemo(() => {
|
||||
return {
|
||||
const scrollContainerStyle = React.useMemo(
|
||||
() => ({
|
||||
width: '100%',
|
||||
height: treeHeight,
|
||||
};
|
||||
}, [treeHeight]);
|
||||
}),
|
||||
[treeHeight],
|
||||
);
|
||||
|
||||
const defaultPlaceholder = React.useMemo(() => {
|
||||
return commentsPanelOptions.defaultPlaceholder;
|
||||
}, [commentsPanelOptions]);
|
||||
const defaultPlaceholder = React.useMemo(() => commentsPanelOptions.defaultPlaceholder, [commentsPanelOptions]);
|
||||
|
||||
const nodes = getRenderTree(treeNodes);
|
||||
|
||||
|
@ -111,8 +114,10 @@ export const CommentsPanel = observer<{ viewState: ViewState; className?: string
|
|||
leftPadding={20}
|
||||
{...commentsPanelOptions.recycleTreeProps}
|
||||
/>
|
||||
) : !defaultPlaceholder || typeof defaultPlaceholder === 'string' ? (
|
||||
<div className={styles.panel_placeholder}>{defaultPlaceholder || localize('comments.panel.placeholder')}</div>
|
||||
) : (
|
||||
(!defaultPlaceholder || typeof defaultPlaceholder === 'string') ? <div className={styles.panel_placeholder}>{defaultPlaceholder || localize('comments.panel.placeholder')}</div> : defaultPlaceholder
|
||||
defaultPlaceholder
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,30 +20,30 @@ const defaultTrigger = '@';
|
|||
const defaultMarkup = '@[__display__](__id__)';
|
||||
const defaultDisplayTransform = (id: string, display: string) => `@${display}`;
|
||||
|
||||
export const CommentsTextArea = React.forwardRef<HTMLTextAreaElement, ICommentTextAreaProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
focusDelay = 0,
|
||||
autoFocus = false,
|
||||
placeholder = '',
|
||||
onFocus,
|
||||
onBlur,
|
||||
onChange,
|
||||
maxRows = 10,
|
||||
minRows = 2,
|
||||
value,
|
||||
initialHeight,
|
||||
dragFiles,
|
||||
} = props;
|
||||
const [ index, setIndex ] = React.useState(0);
|
||||
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
const inputRef = React.useRef<HTMLTextAreaElement | null>(null);
|
||||
const mentionsRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const itemRef = React.useRef<HTMLDivElement | null>(null);
|
||||
// make `ref` to input works
|
||||
React.useImperativeHandle(ref, () => inputRef.current!);
|
||||
export const CommentsTextArea = React.forwardRef<HTMLTextAreaElement, ICommentTextAreaProps>((props, ref) => {
|
||||
const {
|
||||
focusDelay = 0,
|
||||
autoFocus = false,
|
||||
placeholder = '',
|
||||
onFocus,
|
||||
onBlur,
|
||||
onChange,
|
||||
maxRows = 10,
|
||||
minRows = 2,
|
||||
value,
|
||||
initialHeight,
|
||||
dragFiles,
|
||||
} = props;
|
||||
const [index, setIndex] = React.useState(0);
|
||||
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
const inputRef = React.useRef<HTMLTextAreaElement | null>(null);
|
||||
const mentionsRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const itemRef = React.useRef<HTMLDivElement | null>(null);
|
||||
// make `ref` to input works
|
||||
React.useImperativeHandle(ref, () => inputRef.current!);
|
||||
|
||||
const handleFileSelect = React.useCallback(async (event: DragEvent) => {
|
||||
const handleFileSelect = React.useCallback(
|
||||
async (event: DragEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -56,131 +56,135 @@ export const CommentsTextArea = React.forwardRef<HTMLTextAreaElement, ICommentTe
|
|||
inputRef.current.focus();
|
||||
selectLastPosition(inputRef.current.value);
|
||||
}
|
||||
}, [ dragFiles ]);
|
||||
},
|
||||
[dragFiles],
|
||||
);
|
||||
|
||||
const handleDragOver = React.useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}, []);
|
||||
const handleDragOver = React.useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}, []);
|
||||
|
||||
const selectLastPosition = React.useCallback((value) => {
|
||||
const textarea = inputRef.current;
|
||||
if (textarea) {
|
||||
const position = value.toString().length;
|
||||
textarea.setSelectionRange(position, position);
|
||||
}
|
||||
}, []);
|
||||
const selectLastPosition = React.useCallback((value) => {
|
||||
const textarea = inputRef.current;
|
||||
if (textarea) {
|
||||
const position = value.toString().length;
|
||||
textarea.setSelectionRange(position, position);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const textarea = inputRef.current;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
if (initialHeight && textarea.style) {
|
||||
textarea.style.height = initialHeight;
|
||||
}
|
||||
if (focusDelay) {
|
||||
setTimeout(() => {
|
||||
textarea.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}, focusDelay);
|
||||
}
|
||||
// auto set last selection
|
||||
selectLastPosition(value);
|
||||
function handleMouseWheel(event: Event) {
|
||||
const target = event.target as Element;
|
||||
if (target) {
|
||||
if (
|
||||
// 当前文本框出现滚动时,防止被编辑器滚动拦截,阻止冒泡
|
||||
target.nodeName.toUpperCase() === 'TEXTAREA' && target.scrollHeight > target.clientHeight
|
||||
// 当是在弹出的提及里滚动,防止被编辑器滚动拦截,阻止冒泡
|
||||
|| target.nodeName.toUpperCase() === 'UL'
|
||||
|| target.parentElement?.nodeName.toUpperCase() === 'UL'
|
||||
|| target.parentElement?.parentElement?.nodeName.toUpperCase() === 'UL'
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
React.useEffect(() => {
|
||||
const textarea = inputRef.current;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
if (initialHeight && textarea.style) {
|
||||
textarea.style.height = initialHeight;
|
||||
}
|
||||
if (focusDelay) {
|
||||
setTimeout(() => {
|
||||
textarea.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}, focusDelay);
|
||||
}
|
||||
// auto set last selection
|
||||
selectLastPosition(value);
|
||||
function handleMouseWheel(event: Event) {
|
||||
const target = event.target as Element;
|
||||
if (target) {
|
||||
if (
|
||||
// 当前文本框出现滚动时,防止被编辑器滚动拦截,阻止冒泡
|
||||
(target.nodeName.toUpperCase() === 'TEXTAREA' && target.scrollHeight > target.clientHeight) ||
|
||||
// 当是在弹出的提及里滚动,防止被编辑器滚动拦截,阻止冒泡
|
||||
target.nodeName.toUpperCase() === 'UL' ||
|
||||
target.parentElement?.nodeName.toUpperCase() === 'UL' ||
|
||||
target.parentElement?.parentElement?.nodeName.toUpperCase() === 'UL'
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
}
|
||||
mentionsRef.current?.addEventListener('mousewheel', handleMouseWheel, true);
|
||||
return () => {
|
||||
mentionsRef.current?.removeEventListener('mousewheel', handleMouseWheel, true);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
mentionsRef.current?.addEventListener('mousewheel', handleMouseWheel, true);
|
||||
return () => {
|
||||
mentionsRef.current?.removeEventListener('mousewheel', handleMouseWheel, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (index === 0) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}, focusDelay);
|
||||
selectLastPosition(value);
|
||||
}
|
||||
}, [ index ]);
|
||||
React.useEffect(() => {
|
||||
if (index === 0) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}, focusDelay);
|
||||
selectLastPosition(value);
|
||||
}
|
||||
}, [index]);
|
||||
|
||||
const style = React.useMemo(() => {
|
||||
return getMentionBoxStyle({
|
||||
const style = React.useMemo(
|
||||
() =>
|
||||
getMentionBoxStyle({
|
||||
minRows,
|
||||
maxRows,
|
||||
});
|
||||
}, [ minRows, maxRows ]);
|
||||
}),
|
||||
[minRows, maxRows],
|
||||
);
|
||||
|
||||
const mentionsOptions = React.useMemo(() => {
|
||||
return commentsFeatureRegistry.getMentionsOptions();
|
||||
}, [ commentsFeatureRegistry ]);
|
||||
const mentionsOptions = React.useMemo(() => commentsFeatureRegistry.getMentionsOptions(), [commentsFeatureRegistry]);
|
||||
|
||||
const providerData = React.useCallback(async (query: string, callback) => {
|
||||
const providerData = React.useCallback(
|
||||
async (query: string, callback) => {
|
||||
if (mentionsOptions.providerData) {
|
||||
const data = await mentionsOptions.providerData(query);
|
||||
callback(data);
|
||||
} else {
|
||||
callback([]);
|
||||
}
|
||||
}, [ mentionsOptions ]);
|
||||
},
|
||||
[mentionsOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.textarea_container}>
|
||||
<Tabs
|
||||
mini
|
||||
value={index}
|
||||
onChange={(index: number) => setIndex(index)}
|
||||
tabs={[localize('comments.thread.textarea.write'), localize('comments.thread.textarea.preview')]}
|
||||
/>
|
||||
<div>
|
||||
{ index === 0 ? (
|
||||
<div ref={mentionsRef}>
|
||||
<MentionsInput
|
||||
autoFocus={autoFocus}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleFileSelect}
|
||||
inputRef={inputRef}
|
||||
ref={itemRef}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={style}>
|
||||
<Mention
|
||||
markup={mentionsOptions.markup || defaultMarkup}
|
||||
renderSuggestion={mentionsOptions.renderSuggestion}
|
||||
trigger={defaultTrigger}
|
||||
data={providerData}
|
||||
displayTransform={mentionsOptions.displayTransform || defaultDisplayTransform}
|
||||
/>
|
||||
</MentionsInput>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.textarea_preview}>
|
||||
<CommentsBody body={value} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.textarea_container}>
|
||||
<Tabs
|
||||
mini
|
||||
value={index}
|
||||
onChange={(index: number) => setIndex(index)}
|
||||
tabs={[localize('comments.thread.textarea.write'), localize('comments.thread.textarea.preview')]}
|
||||
/>
|
||||
<div>
|
||||
{index === 0 ? (
|
||||
<div ref={mentionsRef}>
|
||||
<MentionsInput
|
||||
autoFocus={autoFocus}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleFileSelect}
|
||||
inputRef={inputRef}
|
||||
ref={itemRef}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={style}
|
||||
>
|
||||
<Mention
|
||||
markup={mentionsOptions.markup || defaultMarkup}
|
||||
renderSuggestion={mentionsOptions.renderSuggestion}
|
||||
trigger={defaultTrigger}
|
||||
data={providerData}
|
||||
displayTransform={mentionsOptions.displayTransform || defaultDisplayTransform}
|
||||
/>
|
||||
</MentionsInput>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.textarea_preview}>
|
||||
<CommentsBody body={value} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import { Injectable, Autowired, INJECTOR_TOKEN, Injector } from '@opensumi/di';
|
||||
import { observable, computed, autorun } from 'mobx';
|
||||
import {
|
||||
IRange,
|
||||
Disposable,
|
||||
URI,
|
||||
IContextKeyService,
|
||||
uuid,
|
||||
localize,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import { IRange, Disposable, URI, IContextKeyService, uuid, localize } from '@opensumi/ide-core-browser';
|
||||
import { CommentsZoneWidget } from './comments-zone.view';
|
||||
import { ICommentsThread, IComment, ICommentsThreadOptions, ICommentsService, IThreadComment, ICommentsZoneWidget } from '../common';
|
||||
import {
|
||||
ICommentsThread,
|
||||
IComment,
|
||||
ICommentsThreadOptions,
|
||||
ICommentsService,
|
||||
IThreadComment,
|
||||
ICommentsZoneWidget,
|
||||
} from '../common';
|
||||
import { IEditor, EditorCollectionService } from '@opensumi/ide-editor';
|
||||
import { ResourceContextKey } from '@opensumi/ide-core-browser/lib/contextkey/resource';
|
||||
|
||||
@Injectable({ multiple: true })
|
||||
export class CommentsThread extends Disposable implements ICommentsThread {
|
||||
|
||||
@Autowired(ICommentsService)
|
||||
commentsService: ICommentsService;
|
||||
|
||||
|
@ -63,10 +62,12 @@ export class CommentsThread extends Disposable implements ICommentsThread {
|
|||
public options: ICommentsThreadOptions,
|
||||
) {
|
||||
super();
|
||||
this.comments = options.comments ? options.comments.map((comment) => ({
|
||||
...comment,
|
||||
id: uuid(),
|
||||
})) : [];
|
||||
this.comments = options.comments
|
||||
? options.comments.map((comment) => ({
|
||||
...comment,
|
||||
id: uuid(),
|
||||
}))
|
||||
: [];
|
||||
this.data = this.options.data;
|
||||
this._contextKeyService = this.registerDispose(this.globalContextKeyService.createScoped());
|
||||
// 设置 resource context key
|
||||
|
@ -76,10 +77,16 @@ export class CommentsThread extends Disposable implements ICommentsThread {
|
|||
this.readOnly = !!options.readOnly;
|
||||
this.label = options.label;
|
||||
this.isCollapsed = !!this.options.isCollapsed;
|
||||
const threadsLengthContext = this._contextKeyService.createKey<number>('threadsLength', this.commentsService.getThreadsByUri(uri).length);
|
||||
const threadsLengthContext = this._contextKeyService.createKey<number>(
|
||||
'threadsLength',
|
||||
this.commentsService.getThreadsByUri(uri).length,
|
||||
);
|
||||
const commentsLengthContext = this._contextKeyService.createKey<number>('commentsLength', this.comments.length);
|
||||
// vscode 用于判断 thread 是否为空
|
||||
const commentThreadIsEmptyContext = this._contextKeyService.createKey<boolean>('commentThreadIsEmpty', !this.comments.length);
|
||||
const commentThreadIsEmptyContext = this._contextKeyService.createKey<boolean>(
|
||||
'commentThreadIsEmpty',
|
||||
!this.comments.length,
|
||||
);
|
||||
// vscode 用于判断是否为当前 controller 注册
|
||||
this._contextKeyService.createKey<string>('commentController', providerId);
|
||||
// 监听 comments 的变化
|
||||
|
@ -95,11 +102,13 @@ export class CommentsThread extends Disposable implements ICommentsThread {
|
|||
}
|
||||
});
|
||||
// 监听每次 thread 的变化,重新设置 threadsLength
|
||||
this.addDispose(this.commentsService.onThreadsChanged((thread) => {
|
||||
if (thread.uri.isEqual(uri)) {
|
||||
threadsLengthContext.set(this.commentsService.getThreadsByUri(uri).length);
|
||||
}
|
||||
}));
|
||||
this.addDispose(
|
||||
this.commentsService.onThreadsChanged((thread) => {
|
||||
if (thread.uri.isEqual(uri)) {
|
||||
threadsLengthContext.set(this.commentsService.getThreadsByUri(uri).length);
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.addDispose({
|
||||
dispose: () => {
|
||||
this.comments = [];
|
||||
|
@ -139,30 +148,29 @@ export class CommentsThread extends Disposable implements ICommentsThread {
|
|||
} else {
|
||||
return localize('comments.zone.title');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getEditorsByUri(uri: URI): IEditor[] {
|
||||
return this.editorCollectionService.listEditors()
|
||||
.filter((editor) => editor.currentUri?.isEqual(uri));
|
||||
return this.editorCollectionService.listEditors().filter((editor) => editor.currentUri?.isEqual(uri));
|
||||
}
|
||||
|
||||
private addWidgetByEditor(editor: IEditor) {
|
||||
const widget = this.injector.get(CommentsZoneWidget, [editor, this]);
|
||||
// 如果当前 widget 发生高度变化,通知同一个 同一个 editor 的其他 range 相同的 thread 也重新计算一下高度
|
||||
this.addDispose(widget.onChangeZoneWidget(() => {
|
||||
const threads = this.commentsService.commentsThreads
|
||||
.filter((thread) => this.isEqual(thread));
|
||||
// 只需要 resize 当前 thread 之后的 thread
|
||||
const currentIndex = threads.findIndex((thread) => thread === this);
|
||||
const resizeThreads = threads.slice(currentIndex + 1);
|
||||
for (const thread of resizeThreads) {
|
||||
if (thread.isShowWidget(editor)) {
|
||||
const widget = thread.getWidgetByEditor(editor);
|
||||
widget?.resize();
|
||||
this.addDispose(
|
||||
widget.onChangeZoneWidget(() => {
|
||||
const threads = this.commentsService.commentsThreads.filter((thread) => this.isEqual(thread));
|
||||
// 只需要 resize 当前 thread 之后的 thread
|
||||
const currentIndex = threads.findIndex((thread) => thread === this);
|
||||
const resizeThreads = threads.slice(currentIndex + 1);
|
||||
for (const thread of resizeThreads) {
|
||||
if (thread.isShowWidget(editor)) {
|
||||
const widget = thread.getWidgetByEditor(editor);
|
||||
widget?.resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
this.addDispose(widget);
|
||||
this.widgets.set(editor, widget);
|
||||
editor.onDispose(() => {
|
||||
|
@ -181,7 +189,7 @@ export class CommentsThread extends Disposable implements ICommentsThread {
|
|||
} else {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public show(editor?: IEditor) {
|
||||
if (editor) {
|
||||
|
@ -267,14 +275,16 @@ export class CommentsThread extends Disposable implements ICommentsThread {
|
|||
}
|
||||
|
||||
public addComment(...comments: IComment[]) {
|
||||
this.comments.push(...comments.map((comment) => ({
|
||||
...comment,
|
||||
id: uuid(),
|
||||
})));
|
||||
this.comments.push(
|
||||
...comments.map((comment) => ({
|
||||
...comment,
|
||||
id: uuid(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
public removeComment(comment: IComment) {
|
||||
const index = this.comments.findIndex((c) => c === comment );
|
||||
const index = this.comments.findIndex((c) => c === comment);
|
||||
if (index !== -1) {
|
||||
this.comments.splice(index, 1);
|
||||
}
|
||||
|
|
|
@ -16,20 +16,14 @@ export class CommentsZoneService extends Disposable {
|
|||
@memoize
|
||||
get commentThreadTitle(): IMenu {
|
||||
return this.registerDispose(
|
||||
this.menuService.createMenu(
|
||||
MenuId.CommentsCommentThreadTitle,
|
||||
this.thread.contextKeyService,
|
||||
),
|
||||
this.menuService.createMenu(MenuId.CommentsCommentThreadTitle, this.thread.contextKeyService),
|
||||
);
|
||||
}
|
||||
|
||||
@memoize
|
||||
get commentThreadContext(): IMenu {
|
||||
return this.registerDispose(
|
||||
this.menuService.createMenu(
|
||||
MenuId.CommentsCommentThreadContext,
|
||||
this.thread.contextKeyService,
|
||||
),
|
||||
this.menuService.createMenu(MenuId.CommentsCommentThreadContext, this.thread.contextKeyService),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,13 @@ import styles from './comments.module.less';
|
|||
import { ConfigProvider, localize, AppConfig, useInjectable, Event, Emitter } from '@opensumi/ide-core-browser';
|
||||
import { CommentItem } from './comments-item.view';
|
||||
import { CommentsTextArea } from './comments-textarea.view';
|
||||
import { ICommentReply, ICommentsZoneWidget, ICommentThreadTitle, ICommentsFeatureRegistry, ICommentsThread } from '../common';
|
||||
import {
|
||||
ICommentReply,
|
||||
ICommentsZoneWidget,
|
||||
ICommentThreadTitle,
|
||||
ICommentsFeatureRegistry,
|
||||
ICommentsThread,
|
||||
} from '../common';
|
||||
import clx from 'classnames';
|
||||
import { InlineActionBar } from '@opensumi/ide-core-browser/lib/components/actions';
|
||||
import { ResizeZoneWidget } from '@opensumi/ide-monaco-enhance';
|
||||
|
@ -20,19 +26,16 @@ export interface ICommentProps {
|
|||
}
|
||||
|
||||
const CommentsZone: React.FC<ICommentProps> = observer(({ thread, widget }) => {
|
||||
const {
|
||||
comments,
|
||||
threadHeaderTitle,
|
||||
contextKeyService,
|
||||
} = thread;
|
||||
const { comments, threadHeaderTitle, contextKeyService } = thread;
|
||||
const injector = useInjectable(INJECTOR_TOKEN);
|
||||
const commentsZoneService: CommentsZoneService = injector.get(CommentsZoneService, [ thread ]);
|
||||
const commentsZoneService: CommentsZoneService = injector.get(CommentsZoneService, [thread]);
|
||||
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
||||
const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
|
||||
const [replyText, setReplyText] = React.useState('');
|
||||
const commentIsEmptyContext = React.useMemo(() => {
|
||||
return contextKeyService.createKey<boolean>('commentIsEmpty', !replyText);
|
||||
}, []);
|
||||
const commentIsEmptyContext = React.useMemo(
|
||||
() => contextKeyService.createKey<boolean>('commentIsEmpty', !replyText),
|
||||
[],
|
||||
);
|
||||
const commentThreadTitle = commentsZoneService.commentThreadTitle;
|
||||
const commentThreadContext = commentsZoneService.commentThreadContext;
|
||||
|
||||
|
@ -42,20 +45,26 @@ const CommentsZone: React.FC<ICommentProps> = observer(({ thread, widget }) => {
|
|||
commentIsEmptyContext.set(!value);
|
||||
}, []);
|
||||
|
||||
const placeholder = React.useMemo(() => {
|
||||
return commentsFeatureRegistry.getProviderFeature(thread.providerId)?.placeholder || `${localize('comments.reply.placeholder')}...`;
|
||||
}, []);
|
||||
const placeholder = React.useMemo(
|
||||
() =>
|
||||
commentsFeatureRegistry.getProviderFeature(thread.providerId)?.placeholder ||
|
||||
`${localize('comments.reply.placeholder')}...`,
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDragFiles = React.useCallback(async (files: FileList) => {
|
||||
if (fileUploadHandler) {
|
||||
const appendText = await fileUploadHandler(replyText, files);
|
||||
setReplyText((text) => {
|
||||
const value = text + appendText;
|
||||
commentIsEmptyContext.set(!value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}, [ replyText ]);
|
||||
const handleDragFiles = React.useCallback(
|
||||
async (files: FileList) => {
|
||||
if (fileUploadHandler) {
|
||||
const appendText = await fileUploadHandler(replyText, files);
|
||||
setReplyText((text) => {
|
||||
const value = text + appendText;
|
||||
commentIsEmptyContext.set(!value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
},
|
||||
[replyText],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const disposer = widget.onFirstDisplay(() => {
|
||||
|
@ -74,44 +83,48 @@ const CommentsZone: React.FC<ICommentProps> = observer(({ thread, widget }) => {
|
|||
<div className={styles.review_title}>{threadHeaderTitle}</div>
|
||||
<InlineActionBar<ICommentThreadTitle>
|
||||
menus={commentThreadTitle}
|
||||
context={[{
|
||||
thread,
|
||||
widget,
|
||||
menuId: MenuId.CommentsCommentThreadTitle,
|
||||
}]}
|
||||
context={[
|
||||
{
|
||||
thread,
|
||||
widget,
|
||||
menuId: MenuId.CommentsCommentThreadTitle,
|
||||
},
|
||||
]}
|
||||
separator='inline'
|
||||
type='icon'/>
|
||||
type='icon'
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.comment_body}>
|
||||
{ comments.length > 0 ?
|
||||
<CommentItem widget={widget} commentThreadContext={commentThreadContext} thread={thread} /> : (
|
||||
<div>
|
||||
<CommentsTextArea
|
||||
focusDelay={100}
|
||||
initialHeight={'auto'}
|
||||
value={replyText}
|
||||
onChange={onChangeReply}
|
||||
placeholder={placeholder}
|
||||
dragFiles={handleDragFiles}
|
||||
/>
|
||||
<div className={styles.comment_bottom_actions}>
|
||||
<InlineActionBar<ICommentReply>
|
||||
className={styles.comment_reply_actions}
|
||||
separator='inline'
|
||||
type='button'
|
||||
context={[
|
||||
{
|
||||
text: replyText,
|
||||
widget,
|
||||
thread,
|
||||
menuId: MenuId.CommentsCommentThreadContext,
|
||||
},
|
||||
]}
|
||||
menus={commentThreadContext}
|
||||
{comments.length > 0 ? (
|
||||
<CommentItem widget={widget} commentThreadContext={commentThreadContext} thread={thread} />
|
||||
) : (
|
||||
<div>
|
||||
<CommentsTextArea
|
||||
focusDelay={100}
|
||||
initialHeight={'auto'}
|
||||
value={replyText}
|
||||
onChange={onChangeReply}
|
||||
placeholder={placeholder}
|
||||
dragFiles={handleDragFiles}
|
||||
/>
|
||||
<div className={styles.comment_bottom_actions}>
|
||||
<InlineActionBar<ICommentReply>
|
||||
className={styles.comment_reply_actions}
|
||||
separator='inline'
|
||||
type='button'
|
||||
context={[
|
||||
{
|
||||
text: replyText,
|
||||
widget,
|
||||
thread,
|
||||
menuId: MenuId.CommentsCommentThreadContext,
|
||||
},
|
||||
]}
|
||||
menus={commentThreadContext}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -119,7 +132,6 @@ const CommentsZone: React.FC<ICommentProps> = observer(({ thread, widget }) => {
|
|||
|
||||
@Injectable({ multiple: true })
|
||||
export class CommentsZoneWidget extends ResizeZoneWidget implements ICommentsZoneWidget {
|
||||
|
||||
@Autowired(AppConfig)
|
||||
appConfig: AppConfig;
|
||||
|
||||
|
@ -146,10 +158,7 @@ export class CommentsZoneWidget extends ResizeZoneWidget implements ICommentsZon
|
|||
const customRender = this.commentsFeatureRegistry.getZoneWidgetRender();
|
||||
ReactDOM.render(
|
||||
<ConfigProvider value={this.appConfig}>
|
||||
{ customRender ?
|
||||
customRender(thread, this) :
|
||||
<CommentsZone thread={thread} widget={this} />
|
||||
}
|
||||
{customRender ? customRender(thread, this) : <CommentsZone thread={thread} widget={this} />}
|
||||
</ConfigProvider>,
|
||||
this._wrapper,
|
||||
);
|
||||
|
@ -190,5 +199,4 @@ export class CommentsZoneWidget extends ResizeZoneWidget implements ICommentsZon
|
|||
protected applyStyle(): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,14 +1,53 @@
|
|||
import { Autowired } from '@opensumi/di';
|
||||
import { Domain, ClientAppContribution, Disposable, localize, ContributionProvider, Event, ToolbarRegistry, CommandContribution, CommandRegistry, getIcon, TabBarToolbarContribution, IEventBus } from '@opensumi/ide-core-browser';
|
||||
import { ICommentsService, CommentPanelId, CommentsContribution, ICommentsFeatureRegistry, CollapseId, CommentPanelCollapse, CloseThreadId, ICommentThreadTitle, SwitchCommandReaction, ICommentsThread, CommentReactionPayload, CommentReactionClick } from '../common';
|
||||
import {
|
||||
Domain,
|
||||
ClientAppContribution,
|
||||
Disposable,
|
||||
localize,
|
||||
ContributionProvider,
|
||||
Event,
|
||||
ToolbarRegistry,
|
||||
CommandContribution,
|
||||
CommandRegistry,
|
||||
getIcon,
|
||||
TabBarToolbarContribution,
|
||||
IEventBus,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import {
|
||||
ICommentsService,
|
||||
CommentPanelId,
|
||||
CommentsContribution,
|
||||
ICommentsFeatureRegistry,
|
||||
CollapseId,
|
||||
CommentPanelCollapse,
|
||||
CloseThreadId,
|
||||
ICommentThreadTitle,
|
||||
SwitchCommandReaction,
|
||||
ICommentsThread,
|
||||
CommentReactionPayload,
|
||||
CommentReactionClick,
|
||||
} from '../common';
|
||||
import { IEditor } from '@opensumi/ide-editor';
|
||||
import { BrowserEditorContribution, IEditorFeatureRegistry } from '@opensumi/ide-editor/lib/browser';
|
||||
import { IMainLayoutService } from '@opensumi/ide-main-layout';
|
||||
import { IMenuRegistry, MenuId, MenuContribution } from '@opensumi/ide-core-browser/lib/menu/next';
|
||||
|
||||
@Domain(ClientAppContribution, BrowserEditorContribution, CommandContribution, TabBarToolbarContribution, MenuContribution)
|
||||
export class CommentsBrowserContribution extends Disposable implements ClientAppContribution, BrowserEditorContribution, CommandContribution, TabBarToolbarContribution, MenuContribution {
|
||||
|
||||
@Domain(
|
||||
ClientAppContribution,
|
||||
BrowserEditorContribution,
|
||||
CommandContribution,
|
||||
TabBarToolbarContribution,
|
||||
MenuContribution,
|
||||
)
|
||||
export class CommentsBrowserContribution
|
||||
extends Disposable
|
||||
implements
|
||||
ClientAppContribution,
|
||||
BrowserEditorContribution,
|
||||
CommandContribution,
|
||||
TabBarToolbarContribution,
|
||||
MenuContribution
|
||||
{
|
||||
@Autowired(ICommentsService)
|
||||
private readonly commentsService: ICommentsService;
|
||||
|
||||
|
@ -36,38 +75,47 @@ export class CommentsBrowserContribution extends Disposable implements ClientApp
|
|||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry) {
|
||||
registry.registerCommand({
|
||||
id: CollapseId,
|
||||
label: '%comments.panel.action.collapse%',
|
||||
iconClass: getIcon('collapse-all'),
|
||||
}, {
|
||||
execute: () => {
|
||||
this.eventBus.fire(new CommentPanelCollapse());
|
||||
registry.registerCommand(
|
||||
{
|
||||
id: CollapseId,
|
||||
label: '%comments.panel.action.collapse%',
|
||||
iconClass: getIcon('collapse-all'),
|
||||
},
|
||||
});
|
||||
{
|
||||
execute: () => {
|
||||
this.eventBus.fire(new CommentPanelCollapse());
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
registry.registerCommand({
|
||||
id: CloseThreadId,
|
||||
label: '%comments.thread.action.close%',
|
||||
iconClass: getIcon('up'),
|
||||
}, {
|
||||
execute: (threadTitle: ICommentThreadTitle) => {
|
||||
const { thread, widget } = threadTitle;
|
||||
if (!thread.comments.length) {
|
||||
thread.dispose();
|
||||
} else {
|
||||
if (widget.isShow) {
|
||||
widget.toggle();
|
||||
registry.registerCommand(
|
||||
{
|
||||
id: CloseThreadId,
|
||||
label: '%comments.thread.action.close%',
|
||||
iconClass: getIcon('up'),
|
||||
},
|
||||
{
|
||||
execute: (threadTitle: ICommentThreadTitle) => {
|
||||
const { thread, widget } = threadTitle;
|
||||
if (!thread.comments.length) {
|
||||
thread.dispose();
|
||||
} else {
|
||||
if (widget.isShow) {
|
||||
widget.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
registry.registerCommand({ id: SwitchCommandReaction }, {
|
||||
execute: (payload: CommentReactionPayload) => {
|
||||
this.eventBus.fire(new CommentReactionClick(payload));
|
||||
registry.registerCommand(
|
||||
{ id: SwitchCommandReaction },
|
||||
{
|
||||
execute: (payload: CommentReactionPayload) => {
|
||||
this.eventBus.fire(new CommentReactionClick(payload));
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
registerMenus(registry: IMenuRegistry): void {
|
||||
|
@ -89,9 +137,11 @@ export class CommentsBrowserContribution extends Disposable implements ClientApp
|
|||
|
||||
private registerCommentsFeature() {
|
||||
this.contributions.getContributions().forEach((contribution, index) => {
|
||||
this.addDispose(this.commentsService.registerCommentRangeProvider(`contribution_${index}`, {
|
||||
getCommentingRanges: (documentModel) => contribution.provideCommentingRanges(documentModel),
|
||||
}));
|
||||
this.addDispose(
|
||||
this.commentsService.registerCommentRangeProvider(`contribution_${index}`, {
|
||||
getCommentingRanges: (documentModel) => contribution.provideCommentingRanges(documentModel),
|
||||
}),
|
||||
);
|
||||
if (contribution.registerCommentsFeature) {
|
||||
contribution.registerCommentsFeature(this.commentsFeatureRegistry);
|
||||
}
|
||||
|
@ -111,17 +161,21 @@ export class CommentsBrowserContribution extends Disposable implements ClientApp
|
|||
});
|
||||
}
|
||||
|
||||
this.addDispose(Event.debounce(this.commentsService.onThreadsChanged, () => {}, 100)(() => {
|
||||
const handler = this.layoutService.getTabbarHandler(CommentPanelId);
|
||||
handler?.setBadge(this.panelBadge);
|
||||
}, this));
|
||||
this.addDispose(
|
||||
Event.debounce(
|
||||
this.commentsService.onThreadsChanged,
|
||||
() => {},
|
||||
100,
|
||||
)(() => {
|
||||
const handler = this.layoutService.getTabbarHandler(CommentPanelId);
|
||||
handler?.setBadge(this.panelBadge);
|
||||
}, this),
|
||||
);
|
||||
}
|
||||
|
||||
registerEditorFeature(registry: IEditorFeatureRegistry) {
|
||||
registry.registerEditorFeatureContribution({
|
||||
contribute: (editor: IEditor) => {
|
||||
return this.commentsService.handleOnCreateEditor(editor);
|
||||
},
|
||||
contribute: (editor: IEditor) => this.commentsService.handleOnCreateEditor(editor),
|
||||
provideEditorOptionsForUri: async (uri) => {
|
||||
const ranges = await this.commentsService.getContributionRanges(uri);
|
||||
|
||||
|
@ -138,5 +192,4 @@ export class CommentsBrowserContribution extends Disposable implements ClientApp
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import * as monaco from '@opensumi/monaco-editor-core/esm/vs/editor/editor.api';
|
||||
import * as textModel from '@opensumi/monaco-editor-core/esm/vs/editor/common/model/textModel';
|
||||
import * as model from '@opensumi/monaco-editor-core/esm/vs/editor/common/model';
|
||||
import {
|
||||
INJECTOR_TOKEN,
|
||||
Injector,
|
||||
Injectable,
|
||||
Autowired,
|
||||
} from '@opensumi/di';
|
||||
import { INJECTOR_TOKEN, Injector, Injectable, Autowired } from '@opensumi/di';
|
||||
import {
|
||||
Disposable,
|
||||
IRange,
|
||||
|
@ -22,7 +17,12 @@ import {
|
|||
Deferred,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import { IEditor } from '@opensumi/ide-editor';
|
||||
import { IEditorDecorationCollectionService, IEditorDocumentModelService, ResourceService, WorkbenchEditorService } from '@opensumi/ide-editor/lib/browser';
|
||||
import {
|
||||
IEditorDecorationCollectionService,
|
||||
IEditorDocumentModelService,
|
||||
ResourceService,
|
||||
WorkbenchEditorService,
|
||||
} from '@opensumi/ide-editor/lib/browser';
|
||||
import {
|
||||
ICommentsService,
|
||||
ICommentsThread,
|
||||
|
@ -45,7 +45,6 @@ import debounce = require('lodash.debounce');
|
|||
|
||||
@Injectable()
|
||||
export class CommentsService extends Disposable implements ICommentsService {
|
||||
|
||||
@Autowired(INJECTOR_TOKEN)
|
||||
private readonly injector: Injector;
|
||||
|
||||
|
@ -135,16 +134,15 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
*/
|
||||
private createThreadDecoration(thread: ICommentsThread): model.IModelDecorationOptions {
|
||||
// 对于新增的空的 thread,默认显示当前用户的头像,否则使用第一个用户的头像
|
||||
const avatar = thread.comments.length === 0 ? this.currentAuthorAvatar : thread.comments[0].author.iconPath?.toString();
|
||||
const avatar =
|
||||
thread.comments.length === 0 ? this.currentAuthorAvatar : thread.comments[0].author.iconPath?.toString();
|
||||
const icon = avatar ? this.iconService.fromIcon('', avatar, IconType.Background) : getIcon('message');
|
||||
const decorationOptions: model.IModelDecorationOptions = {
|
||||
description: 'comments-thread-decoration',
|
||||
// 创建评论显示在 glyph margin 处
|
||||
glyphMarginClassName: ['comments-decoration', 'comments-thread', icon].join(' '),
|
||||
};
|
||||
return textModel.ModelDecorationOptions.createDynamic(
|
||||
decorationOptions,
|
||||
);
|
||||
return textModel.ModelDecorationOptions.createDynamic(decorationOptions);
|
||||
}
|
||||
|
||||
private createHoverDecoration(): model.IModelDecorationOptions {
|
||||
|
@ -152,92 +150,112 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
description: 'comments-hover-decoration',
|
||||
linesDecorationsClassName: ['comments-decoration', 'comments-add', getIcon('message')].join(' '),
|
||||
};
|
||||
return textModel.ModelDecorationOptions.createDynamic(
|
||||
decorationOptions,
|
||||
);
|
||||
return textModel.ModelDecorationOptions.createDynamic(decorationOptions);
|
||||
}
|
||||
|
||||
public init() {
|
||||
// 插件注册 ResourceProvider 时重新注册 CommentDecorationProvider
|
||||
// 例如 Github Pull Request 插件的 scheme 为 pr
|
||||
this.addDispose(this.resourceService.onRegisterResourceProvider((provider) => {
|
||||
if (provider.scheme) {
|
||||
this.shouldShowCommentsSchemes.add(provider.scheme);
|
||||
this.registerDecorationProvider();
|
||||
}
|
||||
}));
|
||||
this.addDispose(this.resourceService.onUnregisterResourceProvider((provider) => {
|
||||
if (provider.scheme) {
|
||||
this.shouldShowCommentsSchemes.delete(provider.scheme);
|
||||
this.registerDecorationProvider();
|
||||
}
|
||||
}));
|
||||
this.addDispose(
|
||||
this.resourceService.onRegisterResourceProvider((provider) => {
|
||||
if (provider.scheme) {
|
||||
this.shouldShowCommentsSchemes.add(provider.scheme);
|
||||
this.registerDecorationProvider();
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.addDispose(
|
||||
this.resourceService.onUnregisterResourceProvider((provider) => {
|
||||
if (provider.scheme) {
|
||||
this.shouldShowCommentsSchemes.delete(provider.scheme);
|
||||
this.registerDecorationProvider();
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.registerDecorationProvider();
|
||||
}
|
||||
|
||||
public handleOnCreateEditor(editor: IEditor) {
|
||||
const disposer = new Disposable();
|
||||
|
||||
disposer.addDispose(editor.monacoEditor.onMouseDown((event) => {
|
||||
if (
|
||||
event.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_DECORATIONS
|
||||
&& event.target.element
|
||||
&& event.target.element.className.indexOf('comments-add') > -1
|
||||
) {
|
||||
const { target } = event;
|
||||
if (target && target.range) {
|
||||
const { range } = target;
|
||||
// 如果已经存在一个待输入的评论组件,则不创建新的
|
||||
if (this.commentsThreads.some((thread) => thread.comments.length === 0 && thread.uri.isEqual(editor.currentUri!) && thread.range.startLineNumber === range.startLineNumber)) {
|
||||
return;
|
||||
disposer.addDispose(
|
||||
editor.monacoEditor.onMouseDown((event) => {
|
||||
if (
|
||||
event.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_DECORATIONS &&
|
||||
event.target.element &&
|
||||
event.target.element.className.indexOf('comments-add') > -1
|
||||
) {
|
||||
const { target } = event;
|
||||
if (target && target.range) {
|
||||
const { range } = target;
|
||||
// 如果已经存在一个待输入的评论组件,则不创建新的
|
||||
if (
|
||||
this.commentsThreads.some(
|
||||
(thread) =>
|
||||
thread.comments.length === 0 &&
|
||||
thread.uri.isEqual(editor.currentUri!) &&
|
||||
thread.range.startLineNumber === range.startLineNumber,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const thread = this.createThread(editor.currentUri!, range);
|
||||
thread.show(editor);
|
||||
}
|
||||
const thread = this.createThread(editor.currentUri!, range);
|
||||
thread.show(editor);
|
||||
}
|
||||
} else if (
|
||||
event.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN
|
||||
&& event.target.element
|
||||
&& event.target.element.className.indexOf('comments-thread') > -1
|
||||
) {
|
||||
const { target } = event;
|
||||
if (target && target.range) {
|
||||
const { range } = target;
|
||||
const threads = this.commentsThreads
|
||||
.filter((thread) => thread.uri.isEqual(editor.currentUri!) && thread.range.startLineNumber === range.startLineNumber);
|
||||
if (threads.length) {
|
||||
// 判断当前 widget 是否是显示的
|
||||
const isShowWidget = threads.some((thread) => thread.isShowWidget(editor));
|
||||
} else if (
|
||||
event.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN &&
|
||||
event.target.element &&
|
||||
event.target.element.className.indexOf('comments-thread') > -1
|
||||
) {
|
||||
const { target } = event;
|
||||
if (target && target.range) {
|
||||
const { range } = target;
|
||||
const threads = this.commentsThreads.filter(
|
||||
(thread) =>
|
||||
thread.uri.isEqual(editor.currentUri!) && thread.range.startLineNumber === range.startLineNumber,
|
||||
);
|
||||
if (threads.length) {
|
||||
// 判断当前 widget 是否是显示的
|
||||
const isShowWidget = threads.some((thread) => thread.isShowWidget(editor));
|
||||
|
||||
if (isShowWidget) {
|
||||
threads.forEach((thread) => thread.hide(editor));
|
||||
} else {
|
||||
threads.forEach((thread) => thread.show(editor));
|
||||
if (isShowWidget) {
|
||||
threads.forEach((thread) => thread.hide(editor));
|
||||
} else {
|
||||
threads.forEach((thread) => thread.show(editor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
let oldDecorations: string[] = [];
|
||||
disposer.addDispose(editor.monacoEditor.onMouseMove(debounce(async (event) => {
|
||||
const uri = editor.currentUri;
|
||||
disposer.addDispose(
|
||||
editor.monacoEditor.onMouseMove(
|
||||
debounce(async (event) => {
|
||||
const uri = editor.currentUri;
|
||||
|
||||
const range = event.target.range;
|
||||
if (uri && range && await this.shouldShowHoverDecoration(uri, range)) {
|
||||
oldDecorations = editor.monacoEditor.deltaDecorations(oldDecorations, [
|
||||
{
|
||||
range: positionToRange(range.startLineNumber),
|
||||
options: this.createHoverDecoration() as unknown as monaco.editor.IModelDecorationOptions,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
oldDecorations = editor.monacoEditor.deltaDecorations(oldDecorations, []);
|
||||
}
|
||||
}, 10)));
|
||||
const range = event.target.range;
|
||||
if (uri && range && (await this.shouldShowHoverDecoration(uri, range))) {
|
||||
oldDecorations = editor.monacoEditor.deltaDecorations(oldDecorations, [
|
||||
{
|
||||
range: positionToRange(range.startLineNumber),
|
||||
options: this.createHoverDecoration() as unknown as monaco.editor.IModelDecorationOptions,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
oldDecorations = editor.monacoEditor.deltaDecorations(oldDecorations, []);
|
||||
}
|
||||
}, 10),
|
||||
),
|
||||
);
|
||||
|
||||
disposer.addDispose(editor.monacoEditor.onMouseLeave(debounce(() => {
|
||||
oldDecorations = editor.monacoEditor.deltaDecorations(oldDecorations, []);
|
||||
}, 10)));
|
||||
disposer.addDispose(
|
||||
editor.monacoEditor.onMouseLeave(
|
||||
debounce(() => {
|
||||
oldDecorations = editor.monacoEditor.deltaDecorations(oldDecorations, []);
|
||||
}, 10),
|
||||
),
|
||||
);
|
||||
|
||||
return disposer;
|
||||
}
|
||||
|
@ -247,12 +265,17 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
return false;
|
||||
}
|
||||
const contributionRanges = await this.getContributionRanges(uri);
|
||||
const isProviderRanges = contributionRanges.some((contributionRange) => range.startLineNumber >= contributionRange.startLineNumber && range.startLineNumber <= contributionRange.endLineNumber);
|
||||
// 如果不支持对同一行进行多个评论,那么过滤掉当前有 thread 行号的 decoration
|
||||
const isShowHoverToSingleLine = this.isMultiCommentsForSingleLine || !this.commentsThreads.some((thread) =>
|
||||
thread.uri.isEqual(uri)
|
||||
&& thread.range.startLineNumber === range.startLineNumber,
|
||||
const isProviderRanges = contributionRanges.some(
|
||||
(contributionRange) =>
|
||||
range.startLineNumber >= contributionRange.startLineNumber &&
|
||||
range.startLineNumber <= contributionRange.endLineNumber,
|
||||
);
|
||||
// 如果不支持对同一行进行多个评论,那么过滤掉当前有 thread 行号的 decoration
|
||||
const isShowHoverToSingleLine =
|
||||
this.isMultiCommentsForSingleLine ||
|
||||
!this.commentsThreads.some(
|
||||
(thread) => thread.uri.isEqual(uri) && thread.range.startLineNumber === range.startLineNumber,
|
||||
);
|
||||
return isProviderRanges && isShowHoverToSingleLine;
|
||||
}
|
||||
|
||||
|
@ -281,10 +304,12 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
}
|
||||
|
||||
public getThreadsByUri(uri: URI) {
|
||||
return this.commentsThreads
|
||||
.filter((thread) => thread.uri.isEqual(uri))
|
||||
// 默认按照 rang 顺序 升序排列
|
||||
.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber);
|
||||
return (
|
||||
this.commentsThreads
|
||||
.filter((thread) => thread.uri.isEqual(uri))
|
||||
// 默认按照 rang 顺序 升序排列
|
||||
.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -312,10 +337,10 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
description: filePath.replace(/^\//, ''),
|
||||
parent: undefined,
|
||||
thread: firstThread,
|
||||
...threads.length && {
|
||||
...(threads.length && {
|
||||
expanded: true,
|
||||
children: [],
|
||||
},
|
||||
}),
|
||||
// 跳过 mobx computed, 强制在走一次 getCommentsPanelTreeNodeHandlers 逻辑
|
||||
_forceUpdateCount: this.forceUpdateCount,
|
||||
};
|
||||
|
@ -338,10 +363,10 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
parent: rootNode,
|
||||
depth: 1,
|
||||
thread,
|
||||
...otherComments.length && {
|
||||
...(otherComments.length && {
|
||||
expanded: true,
|
||||
children: [],
|
||||
},
|
||||
}),
|
||||
comment: firstComment,
|
||||
};
|
||||
const firstCommentChildren = otherComments.map((comment) => {
|
||||
|
@ -386,14 +411,16 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
const rangePromise: Promise<IRange[] | undefined>[] = [];
|
||||
for (const rangeProvider of this.rangeProviderMap) {
|
||||
const [id, provider] = rangeProvider;
|
||||
rangePromise.push((async () => {
|
||||
const ranges = await provider.getCommentingRanges(model?.instance!);
|
||||
if (ranges && ranges.length) {
|
||||
// FIXME: ranges 会被 Diff uri 的两个 range 互相覆盖,导致可能根据行查不到 provider
|
||||
this.rangeOwner.set(id, ranges);
|
||||
}
|
||||
return ranges;
|
||||
})());
|
||||
rangePromise.push(
|
||||
(async () => {
|
||||
const ranges = await provider.getCommentingRanges(model?.instance!);
|
||||
if (ranges && ranges.length) {
|
||||
// FIXME: ranges 会被 Diff uri 的两个 range 互相覆盖,导致可能根据行查不到 provider
|
||||
this.rangeOwner.set(id, ranges);
|
||||
}
|
||||
return ranges;
|
||||
})(),
|
||||
);
|
||||
}
|
||||
const deferredRes = new Deferred<IRange[]>();
|
||||
this.providerDecorationCache.set(uri.toString(), deferredRes);
|
||||
|
@ -413,17 +440,18 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
schemes: [...this.shouldShowCommentsSchemes.values()],
|
||||
key: 'comments',
|
||||
onDidDecorationChange: this.decorationChangeEmitter.event,
|
||||
provideEditorDecoration: (uri: URI) => {
|
||||
return this.commentsThreads.map((thread) => {
|
||||
if (thread.uri.isEqual(uri)) {
|
||||
// 恢复之前的现场
|
||||
thread.showWidgetsIfShowed();
|
||||
} else {
|
||||
// 临时隐藏,当切回来时会恢复
|
||||
thread.hideWidgetsByDispose();
|
||||
}
|
||||
return thread;
|
||||
})
|
||||
provideEditorDecoration: (uri: URI) =>
|
||||
this.commentsThreads
|
||||
.map((thread) => {
|
||||
if (thread.uri.isEqual(uri)) {
|
||||
// 恢复之前的现场
|
||||
thread.showWidgetsIfShowed();
|
||||
} else {
|
||||
// 临时隐藏,当切回来时会恢复
|
||||
thread.hideWidgetsByDispose();
|
||||
}
|
||||
return thread;
|
||||
})
|
||||
.filter((thread) => {
|
||||
const isCurrentThread = thread.uri.isEqual(uri);
|
||||
if (this.filterThreadDecoration) {
|
||||
|
@ -431,13 +459,10 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
}
|
||||
return isCurrentThread;
|
||||
})
|
||||
.map((thread) => {
|
||||
return {
|
||||
range: thread.range,
|
||||
options: this.createThreadDecoration(thread) as unknown as monaco.editor.IModelDecorationOptions,
|
||||
};
|
||||
});
|
||||
},
|
||||
.map((thread) => ({
|
||||
range: thread.range,
|
||||
options: this.createThreadDecoration(thread) as unknown as monaco.editor.IModelDecorationOptions,
|
||||
})),
|
||||
});
|
||||
this.addDispose(this.decorationProviderDisposer);
|
||||
}
|
||||
|
@ -447,17 +472,23 @@ export class CommentsService extends Disposable implements ICommentsService {
|
|||
if (this.layoutService.getTabbarHandler(CommentPanelId)) {
|
||||
return;
|
||||
}
|
||||
this.layoutService.collectTabbarComponent([{
|
||||
id: CommentPanelId,
|
||||
component: CommentsPanel,
|
||||
}], {
|
||||
badge: this.panelBadge,
|
||||
containerId: CommentPanelId,
|
||||
title: localize('comments').toUpperCase(),
|
||||
hidden: false,
|
||||
activateKeyBinding: 'ctrlcmd+shift+c',
|
||||
...this.commentsFeatureRegistry.getCommentsPanelOptions(),
|
||||
}, 'bottom');
|
||||
this.layoutService.collectTabbarComponent(
|
||||
[
|
||||
{
|
||||
id: CommentPanelId,
|
||||
component: CommentsPanel,
|
||||
},
|
||||
],
|
||||
{
|
||||
badge: this.panelBadge,
|
||||
containerId: CommentPanelId,
|
||||
title: localize('comments').toUpperCase(),
|
||||
hidden: false,
|
||||
activateKeyBinding: 'ctrlcmd+shift+c',
|
||||
...this.commentsFeatureRegistry.getCommentsPanelOptions(),
|
||||
},
|
||||
'bottom',
|
||||
);
|
||||
}
|
||||
|
||||
get panelBadge() {
|
||||
|
|
|
@ -1,60 +1,55 @@
|
|||
const lineHeight = 20;
|
||||
|
||||
export const getMentionBoxStyle = ({
|
||||
maxRows = 10,
|
||||
minRows = 2,
|
||||
}) => {
|
||||
return {
|
||||
export const getMentionBoxStyle = ({ maxRows = 10, minRows = 2 }) => ({
|
||||
control: {
|
||||
fontSize: 12,
|
||||
},
|
||||
|
||||
highlighter: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
input: {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
'&multiLine': {
|
||||
control: {
|
||||
fontSize: 12,
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
highlighter: {
|
||||
overflow: 'hidden',
|
||||
padding: 9,
|
||||
},
|
||||
|
||||
input: {
|
||||
margin: 0,
|
||||
boxSizing: 'content-box',
|
||||
padding: '8px 0',
|
||||
lineHeight: `${lineHeight}px`,
|
||||
minHeight: `${lineHeight * minRows}px`,
|
||||
maxHeight: `${lineHeight * maxRows}px`,
|
||||
outline: 0,
|
||||
border: 0,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
|
||||
suggestions: {
|
||||
dataA: 'aaa',
|
||||
list: {
|
||||
backgroundColor: 'var(--kt-selectDropdown-background)',
|
||||
fontSize: 12,
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
|
||||
'&multiLine': {
|
||||
control: {
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
highlighter: {
|
||||
padding: 9,
|
||||
},
|
||||
|
||||
input: {
|
||||
boxSizing: 'content-box',
|
||||
padding: '8px 0',
|
||||
lineHeight: `${ lineHeight }px`,
|
||||
minHeight: `${ lineHeight * minRows }px`,
|
||||
maxHeight: `${ lineHeight * maxRows }px`,
|
||||
outline: 0,
|
||||
border: 0,
|
||||
overflowY: 'auto',
|
||||
item: {
|
||||
backgroundColor: 'var(--kt-selectDropdown-background)',
|
||||
color: 'var(--kt-selectDropdown-foreground)',
|
||||
padding: '4px 16px',
|
||||
'&focused': {
|
||||
backgroundColor: 'var(--kt-selectDropdown-selectionBackground)',
|
||||
},
|
||||
},
|
||||
|
||||
suggestions: {
|
||||
dataA: 'aaa',
|
||||
list: {
|
||||
backgroundColor: 'var(--kt-selectDropdown-background)',
|
||||
fontSize: 12,
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
|
||||
item: {
|
||||
backgroundColor: 'var(--kt-selectDropdown-background)',
|
||||
color: 'var(--kt-selectDropdown-foreground)',
|
||||
padding: '4px 16px',
|
||||
'&focused': {
|
||||
backgroundColor: 'var(--kt-selectDropdown-selectionBackground)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -320,7 +320,7 @@ export type ZoneWidgerRender = (thread: ICommentsThread, widget: ICommentsZoneWi
|
|||
export interface MentionsData {
|
||||
id: string;
|
||||
display: string;
|
||||
[ key: string ]: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MentionsOptions {
|
||||
|
|
|
@ -20,14 +20,9 @@ describe('NewPromptHandle', () => {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
class Folder extends CompositeTreeNode {
|
||||
}
|
||||
class Folder extends CompositeTreeNode {}
|
||||
class File extends TreeNode {
|
||||
constructor(
|
||||
tree: TreeA,
|
||||
parent: Folder | Root,
|
||||
metadata: { [key: string]: string },
|
||||
) {
|
||||
constructor(tree: TreeA, parent: Folder | Root, metadata: { [key: string]: string }) {
|
||||
super(tree, parent, undefined, metadata);
|
||||
}
|
||||
}
|
||||
|
@ -174,14 +169,9 @@ describe('RenamePromptHandle', () => {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
class Folder extends CompositeTreeNode {
|
||||
}
|
||||
class Folder extends CompositeTreeNode {}
|
||||
class File extends TreeNode {
|
||||
constructor(
|
||||
tree: TreeA,
|
||||
parent: Folder | Root,
|
||||
metadata: { [key: string]: string },
|
||||
) {
|
||||
constructor(tree: TreeA, parent: Folder | Root, metadata: { [key: string]: string }) {
|
||||
super(tree, parent, undefined, metadata);
|
||||
}
|
||||
}
|
||||
|
@ -210,5 +200,4 @@ describe('RenamePromptHandle', () => {
|
|||
expect(typeof prompt.onBlur).toBe('function');
|
||||
expect(typeof prompt.onDestroy).toBe('function');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -19,14 +19,9 @@ describe('Tree', () => {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
class Folder extends CompositeTreeNode {
|
||||
}
|
||||
class Folder extends CompositeTreeNode {}
|
||||
class File extends TreeNode {
|
||||
constructor(
|
||||
tree: TreeA,
|
||||
parent: Folder | Root,
|
||||
metadata: { [key: string]: string },
|
||||
) {
|
||||
constructor(tree: TreeA, parent: Folder | Root, metadata: { [key: string]: string }) {
|
||||
super(tree, parent, undefined, metadata);
|
||||
}
|
||||
}
|
||||
|
@ -61,12 +56,12 @@ describe('Tree', () => {
|
|||
expect(file.name).toBe('file_1');
|
||||
});
|
||||
|
||||
it('add new key to Folder\'s metadata', async (done) => {
|
||||
it("add new key to Folder's metadata", async (done) => {
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
const metadata = { name: 'folder' };
|
||||
const folder = new Folder(tree, root, undefined, metadata);
|
||||
expect(folder.name).toBe(metadata.name);
|
||||
root.watcher.on(TreeNodeEvent.DidChangeMetadata, (node, { type, key}) => {
|
||||
root.watcher.on(TreeNodeEvent.DidChangeMetadata, (node, { type, key }) => {
|
||||
if (type === MetadataChangeType.Added && key === 'other') {
|
||||
done();
|
||||
}
|
||||
|
@ -74,12 +69,12 @@ describe('Tree', () => {
|
|||
folder.addMetadata('other', 'hello');
|
||||
});
|
||||
|
||||
it('add new key to File\'s metadata', async (done) => {
|
||||
it("add new key to File's metadata", async (done) => {
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
const metadata = { name: 'folder' };
|
||||
const folder = new Folder(tree, root, undefined, metadata);
|
||||
expect(folder.name).toBe(metadata.name);
|
||||
root.watcher.on(TreeNodeEvent.DidChangeMetadata, (node, { type, key}) => {
|
||||
root.watcher.on(TreeNodeEvent.DidChangeMetadata, (node, { type, key }) => {
|
||||
if (type === MetadataChangeType.Added && key === 'other') {
|
||||
done();
|
||||
}
|
||||
|
@ -90,10 +85,7 @@ describe('Tree', () => {
|
|||
it('ensure root was loaded', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
expect(root.branchSize).toBe(2);
|
||||
});
|
||||
|
@ -101,10 +93,7 @@ describe('Tree', () => {
|
|||
it('force reload root', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
expect(root.branchSize).toBe(2);
|
||||
tree.setPresetChildren([
|
||||
|
@ -119,10 +108,7 @@ describe('Tree', () => {
|
|||
it('expand all node and then collapse all', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'c' }),
|
||||
|
@ -138,24 +124,18 @@ describe('Tree', () => {
|
|||
it('mv b file to a folder', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
const a = root.getTreeNodeAtIndex(0);
|
||||
const b = root.getTreeNodeAtIndex(1);
|
||||
(b as TreeNode).mv((a as CompositeTreeNode));
|
||||
(b as TreeNode).mv(a as CompositeTreeNode);
|
||||
expect((b as TreeNode).parent).toEqual(a);
|
||||
});
|
||||
|
||||
it('insert new item c to a folder', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
expect(root.branchSize).toBe(2);
|
||||
const a = root.getTreeNodeAtIndex(0);
|
||||
|
@ -168,24 +148,18 @@ describe('Tree', () => {
|
|||
it('unlink b file from root', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
expect(root.branchSize).toBe(2);
|
||||
const b = root.getTreeNodeAtIndex(1);
|
||||
(root as CompositeTreeNode).unlinkItem((b as TreeNode));
|
||||
(root as CompositeTreeNode).unlinkItem(b as TreeNode);
|
||||
expect(root.branchSize).toBe(1);
|
||||
});
|
||||
|
||||
it('get node\'s id at index [0]', async () => {
|
||||
it("get node's id at index [0]", async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
expect(root.branchSize).toBe(2);
|
||||
const b = root.getTreeNodeAtIndex(1);
|
||||
|
@ -195,10 +169,7 @@ describe('Tree', () => {
|
|||
it('load nodes while path did not expanded', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'c' }),
|
||||
|
@ -214,23 +185,23 @@ describe('Tree', () => {
|
|||
it('dispath event should be work', async () => {
|
||||
const tree = new TreeA();
|
||||
const root = new Root(tree, undefined, undefined);
|
||||
tree.setPresetChildren([
|
||||
new Folder(tree, root, undefined, { name: 'a' }),
|
||||
new File(tree, root, { name: 'b' }),
|
||||
]);
|
||||
tree.setPresetChildren([new Folder(tree, root, undefined, { name: 'a' }), new File(tree, root, { name: 'b' })]);
|
||||
await root.ensureLoaded();
|
||||
const a = root.getTreeNodeAtIndex(0);
|
||||
const rootWatcher = root?.watchEvents.get(root.path);
|
||||
// mv node
|
||||
await rootWatcher?.callback({ type: WatchEvent.Moved, oldPath: (a as TreeNode).path, newPath: (a as TreeNode).path.replace('a', 'c') });
|
||||
await rootWatcher?.callback({
|
||||
type: WatchEvent.Moved,
|
||||
oldPath: (a as TreeNode).path,
|
||||
newPath: (a as TreeNode).path.replace('a', 'c'),
|
||||
});
|
||||
expect((a as TreeNode).name).toBe('c');
|
||||
// remove node
|
||||
expect(root.branchSize).toBe(2);
|
||||
await rootWatcher?.callback({ type: WatchEvent.Removed, path: (a as TreeNode).path});
|
||||
await rootWatcher?.callback({ type: WatchEvent.Removed, path: (a as TreeNode).path });
|
||||
expect(root.branchSize).toBe(1);
|
||||
// reload nodes
|
||||
await rootWatcher?.callback({ type: WatchEvent.Changed, path: root.path});
|
||||
await rootWatcher?.callback({ type: WatchEvent.Changed, path: root.path });
|
||||
expect(root.branchSize).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -3,10 +3,8 @@ import clx from 'classnames';
|
|||
|
||||
import './styles.less';
|
||||
|
||||
export const Badge: React.FC<{} & React.HTMLAttributes<HTMLSpanElement>> = ({
|
||||
className,
|
||||
children,
|
||||
...restProps
|
||||
}) => {
|
||||
return <span className={clx('kt-badge', className)} {...restProps}>{children}</span>;
|
||||
};
|
||||
export const Badge: React.FC<{} & React.HTMLAttributes<HTMLSpanElement>> = ({ className, children, ...restProps }) => (
|
||||
<span className={clx('kt-badge', className)} {...restProps}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -32,16 +32,27 @@ interface MoreActionProps {
|
|||
|
||||
export type ButtonProps<T> = {
|
||||
htmlType?: ButtonHTMLType;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>
|
||||
} & IButtonBasicProps<T> & MoreActionProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type' | 'onClick'>;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
} & IButtonBasicProps<T> &
|
||||
MoreActionProps &
|
||||
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type' | 'onClick'>;
|
||||
|
||||
const LoadingCircle = () => (
|
||||
<svg viewBox='0 0 1024 1024' focusable='false' className='kt-button-anticon-spin' data-icon='loading' width='1em' height='1em' fill='currentColor' aria-hidden='true'>
|
||||
<svg
|
||||
viewBox='0 0 1024 1024'
|
||||
focusable='false'
|
||||
className='kt-button-anticon-spin'
|
||||
data-icon='loading'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<path d='M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
function noop() { }
|
||||
function noop() {}
|
||||
|
||||
/**
|
||||
* @example
|
||||
|
@ -67,59 +78,90 @@ function noop() { }
|
|||
* <Button<'icon1' | 'icon2'> iconClass=`${customPrefix} icon1` type='icon' />
|
||||
* ```
|
||||
*/
|
||||
export const Button = React.memo(<T extends string>({
|
||||
children,
|
||||
loading,
|
||||
className,
|
||||
type = 'primary',
|
||||
htmlType,
|
||||
size,
|
||||
ghost = false,
|
||||
onClick,
|
||||
disabled,
|
||||
block,
|
||||
iconClass,
|
||||
icon,
|
||||
more,
|
||||
moreIconClass,
|
||||
menu,
|
||||
title,
|
||||
onVisibleChange,
|
||||
...otherProps
|
||||
}: ButtonProps<T>): React.ReactElement<ButtonProps<T>> => {
|
||||
const classes = classNames('kt-button', className, {
|
||||
[`kt-${type}-button-loading`]: loading,
|
||||
[`ghost-${type}-button`]: ghost && !loading && type !== 'link',
|
||||
[`${type}-button`]: type,
|
||||
[`${size}-button-size`]: size,
|
||||
['ghost-button']: ghost && type !== 'link',
|
||||
['block-button']: block,
|
||||
});
|
||||
const iconClesses = classNames(className, {
|
||||
['kt-clickable-icon']: !!onClick,
|
||||
});
|
||||
export const Button = React.memo(
|
||||
<T extends string>({
|
||||
children,
|
||||
loading,
|
||||
className,
|
||||
type = 'primary',
|
||||
htmlType,
|
||||
size,
|
||||
ghost = false,
|
||||
onClick,
|
||||
disabled,
|
||||
block,
|
||||
iconClass,
|
||||
icon,
|
||||
more,
|
||||
moreIconClass,
|
||||
menu,
|
||||
title,
|
||||
onVisibleChange,
|
||||
...otherProps
|
||||
}: ButtonProps<T>): React.ReactElement<ButtonProps<T>> => {
|
||||
const classes = classNames('kt-button', className, {
|
||||
[`kt-${type}-button-loading`]: loading,
|
||||
[`ghost-${type}-button`]: ghost && !loading && type !== 'link',
|
||||
[`${type}-button`]: type,
|
||||
[`${size}-button-size`]: size,
|
||||
['ghost-button']: ghost && type !== 'link',
|
||||
['block-button']: block,
|
||||
});
|
||||
const iconClesses = classNames(className, {
|
||||
['kt-clickable-icon']: !!onClick,
|
||||
});
|
||||
|
||||
if (type === 'icon') {
|
||||
return <Icon tooltip={title} disabled={disabled} icon={icon} onClick={(loading || disabled) ? noop : onClick} className={iconClesses} iconClass={iconClass} {...otherProps} />;
|
||||
}
|
||||
if (type === 'icon') {
|
||||
return (
|
||||
<Icon
|
||||
tooltip={title}
|
||||
disabled={disabled}
|
||||
icon={icon}
|
||||
onClick={loading || disabled ? noop : onClick}
|
||||
className={iconClesses}
|
||||
iconClass={iconClass}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const iconNode = iconClass ? <Icon iconClass={iconClass} disabled={disabled} /> : null;
|
||||
const iconNode = iconClass ? <Icon iconClass={iconClass} disabled={disabled} /> : null;
|
||||
|
||||
if (more) {
|
||||
return (<Dropdown className={'kt-menu'} overlay={menu} trigger={['click']} onVisibleChange={onVisibleChange}>
|
||||
<button {...otherProps} disabled={disabled} className={classes} type={htmlType} onClick={(loading || disabled) ? noop : onClick}>
|
||||
{(loading && type !== 'link') && <LoadingCircle />}
|
||||
if (more) {
|
||||
return (
|
||||
<Dropdown className={'kt-menu'} overlay={menu} trigger={['click']} onVisibleChange={onVisibleChange}>
|
||||
<button
|
||||
{...otherProps}
|
||||
disabled={disabled}
|
||||
className={classes}
|
||||
type={htmlType}
|
||||
onClick={loading || disabled ? noop : onClick}
|
||||
>
|
||||
{loading && type !== 'link' && <LoadingCircle />}
|
||||
{iconNode && iconNode}
|
||||
{children}
|
||||
{more && (
|
||||
<Icon
|
||||
iconClass={moreIconClass ? moreIconClass : getKaitianIcon('down')}
|
||||
className='kt-button-secondary-more'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
{...otherProps}
|
||||
disabled={disabled}
|
||||
className={classes}
|
||||
type={htmlType}
|
||||
onClick={loading || disabled ? noop : onClick}
|
||||
>
|
||||
{loading && type !== 'link' && <LoadingCircle />}
|
||||
{iconNode && iconNode}
|
||||
{children}
|
||||
{more && <Icon iconClass={moreIconClass ? moreIconClass : getKaitianIcon('down')} className='kt-button-secondary-more' />}
|
||||
</button>
|
||||
</Dropdown>);
|
||||
}
|
||||
return (
|
||||
<button {...otherProps} disabled={disabled} className={classes} type={htmlType} onClick={(loading || disabled) ? noop : onClick}>
|
||||
{(loading && type !== 'link') && <LoadingCircle />}
|
||||
{iconNode && iconNode}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
@keyframes loadingCircle {
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,28 +6,19 @@ import './style.less';
|
|||
|
||||
const CheckIconSvg = () => (
|
||||
<svg fill='currentColor' width='1em' height='1em' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z'/>
|
||||
<path d='M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CheckBox: React.FC<React.HTMLProps<HTMLInputElement> & {
|
||||
insertClass?: string;
|
||||
label?: string;
|
||||
size?: 'default' | 'large';
|
||||
disabled?: boolean;
|
||||
}> = ({
|
||||
insertClass,
|
||||
className,
|
||||
label,
|
||||
size = 'default',
|
||||
disabled,
|
||||
checked = false,
|
||||
...restProps
|
||||
}) => {
|
||||
warning(
|
||||
!insertClass,
|
||||
'`insertClass` was deprecated, please use `className` instead',
|
||||
);
|
||||
export const CheckBox: React.FC<
|
||||
React.HTMLProps<HTMLInputElement> & {
|
||||
insertClass?: string;
|
||||
label?: string;
|
||||
size?: 'default' | 'large';
|
||||
disabled?: boolean;
|
||||
}
|
||||
> = ({ insertClass, className, label, size = 'default', disabled, checked = false, ...restProps }) => {
|
||||
warning(!insertClass, '`insertClass` was deprecated, please use `className` instead');
|
||||
|
||||
const cls = classNames('kt-checkbox', insertClass, className, {
|
||||
'kt-checkbox-large': size === 'large',
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
svg {
|
||||
display: block;
|
||||
transition: all .2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
|||
&-large {
|
||||
font-size: @kt-checkbox-large-size;
|
||||
|
||||
&-icon{
|
||||
&-icon {
|
||||
width: @kt-checkbox-large-size;
|
||||
height: @kt-checkbox-large-size;
|
||||
margin-right: 8px;
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
export const ClickOutside: React.FC<{
|
||||
onOutsideClick: (e: MouseEvent) => void;
|
||||
// 目前仅处理 click 和 context menu
|
||||
mouseEvents?: ['click'] | ['contextmenu'] | ['click', 'contextmenu'];
|
||||
} & React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
mouseEvents = ['click'],
|
||||
onOutsideClick,
|
||||
children,
|
||||
...restProps
|
||||
}) => {
|
||||
export const ClickOutside: React.FC<
|
||||
{
|
||||
onOutsideClick: (e: MouseEvent) => void;
|
||||
// 目前仅处理 click 和 context menu
|
||||
mouseEvents?: ['click'] | ['contextmenu'] | ['click', 'contextmenu'];
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
> = ({ mouseEvents = ['click'], onOutsideClick, children, ...restProps }) => {
|
||||
const $containerEl = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const globalClickSpy = React.useCallback((e: MouseEvent) => {
|
||||
if ($containerEl.current && e.target && !$containerEl.current.contains(e.target as any)) {
|
||||
onOutsideClick(e);
|
||||
}
|
||||
}, [onOutsideClick]);
|
||||
const globalClickSpy = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if ($containerEl.current && e.target && !$containerEl.current.contains(e.target as any)) {
|
||||
onOutsideClick(e);
|
||||
}
|
||||
},
|
||||
[onOutsideClick],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
mouseEvents.forEach((event) => {
|
||||
|
@ -27,7 +27,11 @@ export const ClickOutside: React.FC<{
|
|||
window.removeEventListener(event, globalClickSpy, true);
|
||||
});
|
||||
};
|
||||
}, [ mouseEvents ]);
|
||||
}, [mouseEvents]);
|
||||
|
||||
return <div {...restProps} ref={$containerEl}>{children}</div>;
|
||||
return (
|
||||
<div {...restProps} ref={$containerEl}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -33,8 +33,12 @@ export interface IDialogProps extends IOverlayProps {
|
|||
|
||||
const DefaultButtons = ({ onCancel, onOk, cancelText, okText }) => (
|
||||
<>
|
||||
<Button size='large' onClick={onCancel} type='secondary'>{cancelText || '取消'}</Button>
|
||||
<Button size='large' onClick={onOk}>{okText || '确定'}</Button>
|
||||
<Button size='large' onClick={onCancel} type='secondary'>
|
||||
{cancelText || '取消'}
|
||||
</Button>
|
||||
<Button size='large' onClick={onOk}>
|
||||
{okText || '确定'}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -63,20 +67,40 @@ export const Dialog: React.FC<IDialogProps> = ({
|
|||
title={type === 'basic' ? title : null}
|
||||
closable={type === 'basic'}
|
||||
getContainer={getContainer}
|
||||
footer={type === 'basic' ? buttons || <DefaultButtons onCancel={onCancel} onOk={onOk} okText={okText} cancelText={cancelText} /> : undefined}
|
||||
afterClose={afterClose}>
|
||||
footer={
|
||||
type === 'basic'
|
||||
? buttons || <DefaultButtons onCancel={onCancel} onOk={onOk} okText={okText} cancelText={cancelText} />
|
||||
: undefined
|
||||
}
|
||||
afterClose={afterClose}
|
||||
>
|
||||
<>
|
||||
<div className={'kt-dialog-content'}>
|
||||
{icon && <div style={{ color: icon.color }} className={clx('kt-dialog-icon', getKaitianIcon(icon.className) || getIcon(icon.className))}/>}
|
||||
{icon && (
|
||||
<div
|
||||
style={{ color: icon.color }}
|
||||
className={clx('kt-dialog-icon', getKaitianIcon(icon.className) || getIcon(icon.className))}
|
||||
/>
|
||||
)}
|
||||
<div className={'kt-dialog-content_area'}>
|
||||
{type !== 'basic' && title && <p className={'kt-dialog-content_title'}>{title}</p>}
|
||||
{typeof message === 'string' ? (<span className={'kt-dialog-message'}>{ message }</span>) : message}
|
||||
{typeof message === 'string' ? <span className={'kt-dialog-message'}>{message}</span> : message}
|
||||
</div>
|
||||
{closable && type !== 'basic' && <button className={clx('kt-dialog-closex', getKaitianIcon('close'))} onClick={onClose}></button>}
|
||||
{closable && type !== 'basic' && (
|
||||
<button className={clx('kt-dialog-closex', getKaitianIcon('close'))} onClick={onClose}></button>
|
||||
)}
|
||||
</div>
|
||||
{messageType !== MessageType.Empty && type !== 'basic' && <div className={'kt-dialog-buttonWrap'}>
|
||||
{type === 'confirm' ? buttons || <DefaultButtons onCancel={onCancel} onOk={onOk} okText={okText} cancelText={cancelText} /> : <Button size='large' onClick={onClose}>知道了</Button>}
|
||||
</div>}
|
||||
{messageType !== MessageType.Empty && type !== 'basic' && (
|
||||
<div className={'kt-dialog-buttonWrap'}>
|
||||
{type === 'confirm' ? (
|
||||
buttons || <DefaultButtons onCancel={onCancel} onOk={onOk} okText={okText} cancelText={cancelText} />
|
||||
) : (
|
||||
<Button size='large' onClick={onClose}>
|
||||
知道了
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Overlay>
|
||||
);
|
||||
|
|
|
@ -5,15 +5,8 @@ import RightOutlined from '@ant-design/icons/RightOutlined';
|
|||
import { tuple } from '../utils/type';
|
||||
import { warning } from '../utils/warning';
|
||||
|
||||
const Placements = tuple(
|
||||
'topLeft',
|
||||
'topCenter',
|
||||
'topRight',
|
||||
'bottomLeft',
|
||||
'bottomCenter',
|
||||
'bottomRight',
|
||||
);
|
||||
type Placement = (typeof Placements)[number];
|
||||
const Placements = tuple('topLeft', 'topCenter', 'topRight', 'bottomLeft', 'bottomCenter', 'bottomRight');
|
||||
type Placement = typeof Placements[number];
|
||||
|
||||
type OverlayFunc = () => React.ReactNode;
|
||||
|
||||
|
@ -110,16 +103,10 @@ export default class Dropdown extends React.Component<DropDownProps, any> {
|
|||
});
|
||||
|
||||
return fixedModeOverlay;
|
||||
}
|
||||
};
|
||||
|
||||
renderDropDown = () => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
children,
|
||||
trigger,
|
||||
disabled,
|
||||
getPopupContainer,
|
||||
} = this.props;
|
||||
const { prefixCls: customizePrefixCls, children, trigger, disabled, getPopupContainer } = this.props;
|
||||
|
||||
const prefixCls = customizePrefixCls || 'kt-dropdown';
|
||||
const child = React.Children.only(children) as React.ReactElement<any>;
|
||||
|
@ -148,7 +135,7 @@ export default class Dropdown extends React.Component<DropDownProps, any> {
|
|||
{dropdownTrigger}
|
||||
</RcDropdown>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.renderDropDown();
|
||||
|
|
|
@ -26,9 +26,7 @@ export const IconContext = React.createContext<IiconContext<any>>({
|
|||
export function IconContextProvider(props: React.PropsWithChildren<{ value: IiconContext<any> }>) {
|
||||
return (
|
||||
<IconContext.Provider value={props.value}>
|
||||
<IconContext.Consumer>
|
||||
{(value) => props.value === value ? props.children : null}
|
||||
</IconContext.Consumer>
|
||||
<IconContext.Consumer>{(value) => (props.value === value ? props.children : null)}</IconContext.Consumer>
|
||||
</IconContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -63,16 +61,22 @@ export type IconProps<T = any> = IconBaseProps<T> & React.HTMLAttributes<HTMLSpa
|
|||
* ```
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line:only-arrow-functions
|
||||
const IconBase = function<T>(
|
||||
props: IconProps<T>,
|
||||
ref: React.Ref<HTMLSpanElement>,
|
||||
) {
|
||||
const IconBase = function <T>(props: IconProps<T>, ref: React.Ref<HTMLSpanElement>) {
|
||||
const {
|
||||
size = 'small', loading, icon,
|
||||
iconClass, className, tooltip,
|
||||
rotate, anim, fill, disabled,
|
||||
onClick, children, resourceOptions, ...restProps
|
||||
size = 'small',
|
||||
loading,
|
||||
icon,
|
||||
iconClass,
|
||||
className,
|
||||
tooltip,
|
||||
rotate,
|
||||
anim,
|
||||
fill,
|
||||
disabled,
|
||||
onClick,
|
||||
children,
|
||||
resourceOptions,
|
||||
...restProps
|
||||
} = props;
|
||||
const iconShapeOptions = { rotate, anim, fill };
|
||||
|
||||
|
@ -104,19 +108,14 @@ const IconBase = function<T>(
|
|||
title={tooltip}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
className={clx(
|
||||
'kt-icon',
|
||||
iconClx,
|
||||
className,
|
||||
{
|
||||
'kt-icon-loading': loading,
|
||||
'kt-icon-disabled': !!disabled,
|
||||
[`kt-icon-${size}`]: !!size,
|
||||
'kt-icon-clickable': !!onClick,
|
||||
'kt-icon-resource': !!resourceOptions,
|
||||
'expanded': !!resourceOptions && resourceOptions.isOpenedDirectory,
|
||||
},
|
||||
)}
|
||||
className={clx('kt-icon', iconClx, className, {
|
||||
'kt-icon-loading': loading,
|
||||
'kt-icon-disabled': !!disabled,
|
||||
[`kt-icon-${size}`]: !!size,
|
||||
'kt-icon-clickable': !!onClick,
|
||||
'kt-icon-resource': !!resourceOptions,
|
||||
expanded: !!resourceOptions && resourceOptions.isOpenedDirectory,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
@ -126,7 +125,7 @@ const IconBase = function<T>(
|
|||
// for ts type usage for iconfont-cn.tsx
|
||||
export const _InternalIcon = React.memo(React.forwardRef<HTMLSpanElement, IconProps>(IconBase));
|
||||
|
||||
export const Icon = React.memo(React.forwardRef<HTMLSpanElement, IconProps>(IconBase)) as <T = any> (
|
||||
export const Icon = React.memo(React.forwardRef<HTMLSpanElement, IconProps>(IconBase)) as <T = any>(
|
||||
props: IconProps<T>,
|
||||
ref: React.Ref<HTMLSpanElement>,
|
||||
) => React.ReactElement;
|
||||
|
|
|
@ -18,14 +18,10 @@ export interface CustomIconOptions {
|
|||
export type IconFontProps<T> = Omit<IconProps<T>, 'iconClass'>;
|
||||
|
||||
function isValidCustomScriptUrl(scriptUrl: string): boolean {
|
||||
return Boolean(
|
||||
typeof scriptUrl === 'string'
|
||||
&& scriptUrl.length
|
||||
&& !customCache.has(scriptUrl),
|
||||
);
|
||||
return Boolean(typeof scriptUrl === 'string' && scriptUrl.length && !customCache.has(scriptUrl));
|
||||
}
|
||||
|
||||
function createScriptUrlElements(scriptUrls: string[], index: number = 0): void {
|
||||
function createScriptUrlElements(scriptUrls: string[], index = 0): void {
|
||||
const currentScriptUrl = scriptUrls[index];
|
||||
if (isValidCustomScriptUrl(currentScriptUrl)) {
|
||||
const script = document.createElement('script');
|
||||
|
@ -73,42 +69,34 @@ export function createFromIconfontCN<T>(options: CustomIconOptions = {}): React.
|
|||
}
|
||||
}
|
||||
|
||||
const IconFont = React.forwardRef<HTMLSpanElement, IconProps>((
|
||||
props: IconProps<T>,
|
||||
ref: React.Ref<HTMLSpanElement>,
|
||||
) => {
|
||||
const { icon, children, rotate, anim, fill, className = '', ...restProps } = props;
|
||||
const iconShapeOptions = { rotate, anim, fill };
|
||||
const IconFont = React.forwardRef<HTMLSpanElement, IconProps>(
|
||||
(props: IconProps<T>, ref: React.Ref<HTMLSpanElement>) => {
|
||||
const { icon, children, rotate, anim, fill, className = '', ...restProps } = props;
|
||||
const iconShapeOptions = { rotate, anim, fill };
|
||||
|
||||
// children > icon
|
||||
let content: React.ReactNode = null;
|
||||
if (icon) {
|
||||
content = (
|
||||
<svg {...svgBaseProps} focusable='false'>
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
// children > icon
|
||||
let content: React.ReactNode = null;
|
||||
if (icon) {
|
||||
content = (
|
||||
<svg {...svgBaseProps} focusable='false'>
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (children) {
|
||||
content = children;
|
||||
}
|
||||
|
||||
const iconShapeClx = getIconShapeClxList(iconShapeOptions);
|
||||
return (
|
||||
<_InternalIcon {...extraCommonProps} {...restProps} className={clx(className, iconShapeClx)} ref={ref}>
|
||||
{content}
|
||||
</_InternalIcon>
|
||||
);
|
||||
}
|
||||
if (children) {
|
||||
content = children;
|
||||
}
|
||||
|
||||
const iconShapeClx = getIconShapeClxList(iconShapeOptions);
|
||||
return (
|
||||
<_InternalIcon
|
||||
{...extraCommonProps}
|
||||
{...restProps}
|
||||
className={clx(className, iconShapeClx)}
|
||||
ref={ref}>
|
||||
{content}
|
||||
</_InternalIcon>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
IconFont.displayName = 'Iconfont';
|
||||
|
||||
return IconFont as <T = any> (
|
||||
props: IconProps<T>,
|
||||
ref: React.Ref<HTMLSpanElement>,
|
||||
) => React.ReactElement;
|
||||
return IconFont as <T = any>(props: IconProps<T>, ref: React.Ref<HTMLSpanElement>) => React.ReactElement;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
|
||||
&-loading {
|
||||
animation: ktIconloadingCircle 1s infinite linear;
|
||||
}
|
||||
|
@ -22,7 +21,6 @@
|
|||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
&-clickable {
|
||||
&:hover {
|
||||
// transform: scale(1.2);
|
||||
|
@ -44,11 +42,10 @@
|
|||
|
||||
@keyframes ktIconloadingCircle {
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Animated Icons
|
||||
// --------------------------
|
||||
|
||||
|
@ -73,22 +70,35 @@
|
|||
// Rotated & Flipped Icons Mixins
|
||||
// -------------------------
|
||||
.icon-anim-rotate(@degrees, @rotation) {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{@rotation})";
|
||||
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{@rotation})';
|
||||
transform: rotate(@degrees);
|
||||
}
|
||||
|
||||
.icon-anim-flip(@horiz, @vert, @rotation) {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{@rotation}, mirror=1)";
|
||||
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{@rotation}, mirror=1)';
|
||||
transform: scale(@horiz, @vert);
|
||||
}
|
||||
|
||||
.iconfont-rotate-90 { .icon-anim-rotate(90deg, 1); }
|
||||
.iconfont-rotate-180 { .icon-anim-rotate(180deg, 2); }
|
||||
.iconfont-rotate-270 { .icon-anim-rotate(270deg, 3); }
|
||||
.iconfont-rotate-90 {
|
||||
.icon-anim-rotate(90deg, 1);
|
||||
}
|
||||
.iconfont-rotate-180 {
|
||||
.icon-anim-rotate(180deg, 2);
|
||||
}
|
||||
.iconfont-rotate-270 {
|
||||
.icon-anim-rotate(270deg, 3);
|
||||
}
|
||||
|
||||
.iconfont-flip-horizontal { .icon-anim-flip(-1, 1, 0); }
|
||||
.iconfont-flip-vertical { .icon-anim-flip(1, -1, 2); }
|
||||
.iconfont-flip-both, .iconfont-flip-horizontal.iconfont-flip-vertical { .icon-anim-flip(-1, -1, 2); }
|
||||
.iconfont-flip-horizontal {
|
||||
.icon-anim-flip(-1, 1, 0);
|
||||
}
|
||||
.iconfont-flip-vertical {
|
||||
.icon-anim-flip(1, -1, 2);
|
||||
}
|
||||
.iconfont-flip-both,
|
||||
.iconfont-flip-horizontal.iconfont-flip-vertical {
|
||||
.icon-anim-flip(-1, -1, 2);
|
||||
}
|
||||
|
||||
// Hook for IE8-9
|
||||
// -------------------------
|
||||
|
|
|
@ -19,20 +19,19 @@ export interface IHistoryInputBoxHandler {
|
|||
}
|
||||
|
||||
export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
||||
|
||||
private inputRef = React.createRef<any>();
|
||||
public history: HistoryNavigator<string>;
|
||||
public inputProps: HistoryInputBoxProp;
|
||||
|
||||
public readonly state: {
|
||||
inputValue: '',
|
||||
inputValue: '';
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
const { history, onReady } = this.props;
|
||||
|
||||
this.history = new HistoryNavigator(history || [], 100);
|
||||
this.inputProps = {...this.props};
|
||||
this.inputProps = { ...this.props };
|
||||
delete this.inputProps.onReady;
|
||||
|
||||
this.setState({
|
||||
|
@ -56,11 +55,9 @@ export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
|||
if (this.history && v && v !== this.getCurrentValue()) {
|
||||
this.history.add(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public getHistory = (): string[] => {
|
||||
return this.history && this.history.getHistory() || [];
|
||||
}
|
||||
public getHistory = (): string[] => (this.history && this.history.getHistory()) || [];
|
||||
|
||||
public showNextValue = () => {
|
||||
const value = this.getCurrentValue() || '';
|
||||
|
@ -76,7 +73,7 @@ export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
|||
if (next) {
|
||||
this.onValueChange(next);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public showPreviousValue = (): void => {
|
||||
const value = this.getCurrentValue() || '';
|
||||
|
@ -92,11 +89,11 @@ export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
|||
if (previous) {
|
||||
this.onValueChange(previous);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public clearHistory = (): void => {
|
||||
this.history && this.history.clear();
|
||||
}
|
||||
};
|
||||
|
||||
public getCurrentValue = (): string | null => {
|
||||
if (!this.history) {
|
||||
|
@ -109,15 +106,12 @@ export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
|||
this.history.next();
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
};
|
||||
|
||||
public getPreviousValue = (): string | null => {
|
||||
return this.history && (this.history.previous() || this.history.first()) || null;
|
||||
}
|
||||
public getPreviousValue = (): string | null =>
|
||||
(this.history && (this.history.previous() || this.history.first())) || null;
|
||||
|
||||
public getNextValue = (): string | null => {
|
||||
return this.history && (this.history.next() || this.history.last()) || null;
|
||||
}
|
||||
public getNextValue = (): string | null => (this.history && (this.history.next() || this.history.last())) || null;
|
||||
|
||||
public onValueChange = (v: string) => {
|
||||
const { onValueChange } = this.props;
|
||||
|
@ -129,7 +123,7 @@ export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
|||
if (onValueChange) {
|
||||
onValueChange(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyDown = (e) => {
|
||||
const { onKeyDown } = this.props;
|
||||
|
@ -137,16 +131,24 @@ export class HistoryInputBox extends React.Component<HistoryInputBoxProp> {
|
|||
if (onKeyDown) {
|
||||
onKeyDown(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private focus = () => {
|
||||
if (this.inputRef && this.inputRef.current) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const inputValue = this.state && this.state.inputValue;
|
||||
return <Input ref={this.inputRef} {...this.inputProps} onValueChange={this.onValueChange} onKeyDown={this.onKeyDown} value={inputValue}/>;
|
||||
return (
|
||||
<Input
|
||||
ref={this.inputRef}
|
||||
{...this.inputProps}
|
||||
onValueChange={this.onValueChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
value={inputValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Icon } from '../icon';
|
|||
import './input.less';
|
||||
|
||||
function isUndefined(obj: any): obj is undefined {
|
||||
return typeof (obj) === 'undefined';
|
||||
return typeof obj === 'undefined';
|
||||
}
|
||||
|
||||
export interface InputSelection {
|
||||
|
@ -34,7 +34,7 @@ export interface IInputBaseProps extends Omit<React.InputHTMLAttributes<HTMLInpu
|
|||
/**
|
||||
* @default true
|
||||
* 保持 focus,即使点击到 addon 部分
|
||||
*/
|
||||
*/
|
||||
persistFocus?: boolean;
|
||||
/**
|
||||
* @default false
|
||||
|
@ -77,165 +77,157 @@ function resolveOnChange(
|
|||
}
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, IInputBaseProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
defaultValue,
|
||||
className,
|
||||
wrapperStyle,
|
||||
size = 'default',
|
||||
controls,
|
||||
onChange,
|
||||
selection,
|
||||
addonBefore,
|
||||
addonAfter,
|
||||
persistFocus = true,
|
||||
hasClear,
|
||||
afterClear,
|
||||
value = '',
|
||||
onValueChange,
|
||||
onPressEnter,
|
||||
onKeyDown,
|
||||
...restProps
|
||||
} = props;
|
||||
export const Input = React.forwardRef<HTMLInputElement, IInputBaseProps>((props, ref) => {
|
||||
const {
|
||||
defaultValue,
|
||||
className,
|
||||
wrapperStyle,
|
||||
size = 'default',
|
||||
controls,
|
||||
onChange,
|
||||
selection,
|
||||
addonBefore,
|
||||
addonAfter,
|
||||
persistFocus = true,
|
||||
hasClear,
|
||||
afterClear,
|
||||
value = '',
|
||||
onValueChange,
|
||||
onPressEnter,
|
||||
onKeyDown,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
warning(
|
||||
!controls,
|
||||
'[@opensumi/ide-components Input]: `controls` was deprecated, please use `addonAfter` instead',
|
||||
);
|
||||
warning(!controls, '[@opensumi/ide-components Input]: `controls` was deprecated, please use `addonAfter` instead');
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const [isDirty, setIsDirty] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const [isDirty, setIsDirty] = React.useState(false);
|
||||
|
||||
// handle initial value from `value` or `defaultValue`
|
||||
const [inputValue, setInputValue] = React.useState<string>(() => ((value ?? defaultValue) || ''));
|
||||
const [preValue, setPrevValue] = React.useState<string>(() => ((value ?? defaultValue) || ''));
|
||||
// handle initial value from `value` or `defaultValue`
|
||||
const [inputValue, setInputValue] = React.useState<string>(() => (value ?? defaultValue) || '');
|
||||
const [preValue, setPrevValue] = React.useState<string>(() => (value ?? defaultValue) || '');
|
||||
|
||||
// make `ref` to input works
|
||||
React.useImperativeHandle(ref, () => inputRef.current!);
|
||||
// make `ref` to input works
|
||||
React.useImperativeHandle(ref, () => inputRef.current!);
|
||||
|
||||
// handle `selection`
|
||||
React.useEffect(() => {
|
||||
if (selection && !isUndefined(selection.start) && !isDirty) {
|
||||
inputRef.current!.setSelectionRange(selection.start, selection.end);
|
||||
}
|
||||
}, [selection, isDirty]);
|
||||
// handle `selection`
|
||||
React.useEffect(() => {
|
||||
if (selection && !isUndefined(selection.start) && !isDirty) {
|
||||
inputRef.current!.setSelectionRange(selection.start, selection.end);
|
||||
}
|
||||
}, [selection, isDirty]);
|
||||
|
||||
// implements for `getDerivedStateFromProps` to update `state#inputValue` from `props#value`
|
||||
React.useEffect(() => {
|
||||
// what if value is null??
|
||||
// 如果不加这一句的话,后面又会把 inputValue 设置成 null
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
// implements for `getDerivedStateFromProps` to update `state#inputValue` from `props#value`
|
||||
React.useEffect(() => {
|
||||
// what if value is null??
|
||||
// 如果不加这一句的话,后面又会把 inputValue 设置成 null
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value !== preValue && value !== inputValue) {
|
||||
setInputValue(value);
|
||||
}
|
||||
|
||||
// save prev props into state
|
||||
if (value !== preValue) {
|
||||
setPrevValue(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
triggerChange(e.target.value);
|
||||
resolveOnChange(inputRef.current!, e, onChange);
|
||||
};
|
||||
|
||||
const handleClearIconClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
triggerChange('');
|
||||
resolveOnChange(inputRef.current!, e, onChange);
|
||||
if (typeof afterClear === 'function') {
|
||||
afterClear();
|
||||
}
|
||||
};
|
||||
|
||||
const triggerChange = (value: string) => {
|
||||
if (value !== preValue && value !== inputValue) {
|
||||
setInputValue(value);
|
||||
}
|
||||
|
||||
if (typeof onValueChange === 'function') {
|
||||
onValueChange(value);
|
||||
}
|
||||
// save prev props into state
|
||||
if (value !== preValue) {
|
||||
setPrevValue(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// trigger `dirty` state
|
||||
if (!isDirty) {
|
||||
setIsDirty(true);
|
||||
}
|
||||
};
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
triggerChange(e.target.value);
|
||||
resolveOnChange(inputRef.current!, e, onChange);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.keyCode === 13 && typeof onPressEnter === 'function') {
|
||||
onPressEnter(e);
|
||||
}
|
||||
if (typeof onKeyDown === 'function') {
|
||||
onKeyDown(e);
|
||||
}
|
||||
};
|
||||
const handleClearIconClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
triggerChange('');
|
||||
resolveOnChange(inputRef.current!, e, onChange);
|
||||
if (typeof afterClear === 'function') {
|
||||
afterClear();
|
||||
}
|
||||
};
|
||||
|
||||
// addonAfter 优先级高于被废弃的 controls 属性
|
||||
const addonAfterNode = addonAfter || controls;
|
||||
const triggerChange = (value: string) => {
|
||||
setInputValue(value);
|
||||
|
||||
const persistFocusProps = persistFocus
|
||||
? {
|
||||
if (typeof onValueChange === 'function') {
|
||||
onValueChange(value);
|
||||
}
|
||||
|
||||
// trigger `dirty` state
|
||||
if (!isDirty) {
|
||||
setIsDirty(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.keyCode === 13 && typeof onPressEnter === 'function') {
|
||||
onPressEnter(e);
|
||||
}
|
||||
if (typeof onKeyDown === 'function') {
|
||||
onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
// addonAfter 优先级高于被废弃的 controls 属性
|
||||
const addonAfterNode = addonAfter || controls;
|
||||
|
||||
const persistFocusProps = persistFocus
|
||||
? {
|
||||
onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
} : {};
|
||||
|
||||
const addonRender = (addonNodes: React.ReactNode | undefined, klassName: string) => {
|
||||
if (!addonNodes) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={clx('kt-input-addon', klassName)} {...persistFocusProps}>
|
||||
{
|
||||
React.Children.map(addonNodes, (child) =>
|
||||
React.isValidElement(child)
|
||||
? React.cloneElement(child!, persistFocusProps)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const inputClx = clx('kt-input', className, {
|
||||
[`kt-input-${size}`]: size,
|
||||
['kt-input-disabled']: props.disabled,
|
||||
});
|
||||
: {};
|
||||
|
||||
const addonRender = (addonNodes: React.ReactNode | undefined, klassName: string) => {
|
||||
if (!addonNodes) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={inputClx} style={wrapperStyle}>
|
||||
{addonRender(addonBefore, 'kt-input-addon-before')}
|
||||
<div className='kt-input-box'>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
autoCapitalize='off'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck={false}
|
||||
{...restProps}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
{
|
||||
hasClear && inputValue
|
||||
&& <Icon
|
||||
className='kt-input-clear'
|
||||
icon='close-circle-fill'
|
||||
onClick={handleClearIconClick}
|
||||
{...persistFocusProps} />
|
||||
}
|
||||
</div>
|
||||
{addonRender(addonAfterNode, 'kt-input-addon-after')}
|
||||
<div className={clx('kt-input-addon', klassName)} {...persistFocusProps}>
|
||||
{React.Children.map(addonNodes, (child) =>
|
||||
React.isValidElement(child) ? React.cloneElement(child!, persistFocusProps) : null,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const inputClx = clx('kt-input', className, {
|
||||
[`kt-input-${size}`]: size,
|
||||
['kt-input-disabled']: props.disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={inputClx} style={wrapperStyle}>
|
||||
{addonRender(addonBefore, 'kt-input-addon-before')}
|
||||
<div className='kt-input-box'>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
autoCapitalize='off'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck={false}
|
||||
{...restProps}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
{hasClear && inputValue && (
|
||||
<Icon
|
||||
className='kt-input-clear'
|
||||
icon='close-circle-fill'
|
||||
onClick={handleClearIconClick}
|
||||
{...persistFocusProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{addonRender(addonAfterNode, 'kt-input-addon-after')}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = 'KTInput';
|
||||
|
|
|
@ -4,8 +4,6 @@ export interface ITextAreaProps {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export const TextArea: React.FC<ITextAreaProps> = () => {
|
||||
return <textarea name='' id='' cols={30} rows={10}></textarea>;
|
||||
};
|
||||
export const TextArea: React.FC<ITextAreaProps> = () => <textarea name='' id='' cols={30} rows={10}></textarea>;
|
||||
|
||||
TextArea.displayName = 'KTTextArea';
|
||||
|
|
|
@ -26,73 +26,68 @@ export interface ValidateInputProp extends IInputBaseProps {
|
|||
popup?: boolean;
|
||||
}
|
||||
|
||||
export const ValidateInput = React.forwardRef<HTMLInputElement, ValidateInputProp>((
|
||||
{
|
||||
className,
|
||||
validate,
|
||||
onChange,
|
||||
onValueChange,
|
||||
validateMessage: validateInfo,
|
||||
popup = true,
|
||||
...restProps
|
||||
export const ValidateInput = React.forwardRef<HTMLInputElement, ValidateInputProp>(
|
||||
(
|
||||
{ className, validate, onChange, onValueChange, validateMessage: validateInfo, popup = true, ...restProps },
|
||||
ref: React.MutableRefObject<HTMLInputElement>,
|
||||
) => {
|
||||
const [validateMessage, setValidateMessage] = React.useState<ValidateMessage | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
setValidateMessage(validateInfo);
|
||||
}, [validateInfo]);
|
||||
|
||||
warning(
|
||||
!validateMessage || validateMessage.type !== VALIDATE_TYPE.WRANING,
|
||||
'`VALIDATE_TYPE.WRANING` was a wrong typo, please use `VALIDATE_TYPE.WARNING` instead',
|
||||
);
|
||||
|
||||
const validateClx = classNames({
|
||||
'validate-error': validateMessage && validateMessage.type === VALIDATE_TYPE.ERROR,
|
||||
'validate-warning':
|
||||
validateMessage && [VALIDATE_TYPE.WRANING, VALIDATE_TYPE.WARNING].includes(validateMessage.type),
|
||||
'validate-info': validateMessage && validateMessage.type === VALIDATE_TYPE.INFO,
|
||||
});
|
||||
|
||||
const renderValidateMessage = () => {
|
||||
if (validateMessage && validateMessage.message) {
|
||||
return <div className={classNames('validate-message', validateClx, { popup })}>{validateMessage.message}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const input: HTMLInputElement = event.target;
|
||||
let value;
|
||||
if (input.type === 'number') {
|
||||
value = event.target.valueAsNumber;
|
||||
} else {
|
||||
value = event.target.value;
|
||||
}
|
||||
if (typeof validate === 'function') {
|
||||
const message = validate(value);
|
||||
setValidateMessage(message);
|
||||
}
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(event);
|
||||
}
|
||||
if (typeof onValueChange === 'function') {
|
||||
onValueChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('input-box', { popup })}>
|
||||
<Input
|
||||
type='text'
|
||||
ref={ref}
|
||||
className={classNames(className, validateMessage, validateClx)}
|
||||
onChange={handleChange}
|
||||
{...restProps}
|
||||
/>
|
||||
{renderValidateMessage()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
ref: React.MutableRefObject<HTMLInputElement>,
|
||||
) => {
|
||||
const [validateMessage, setValidateMessage] = React.useState<ValidateMessage | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
setValidateMessage(validateInfo);
|
||||
}, [validateInfo]);
|
||||
|
||||
warning(
|
||||
!validateMessage || validateMessage.type !== VALIDATE_TYPE.WRANING,
|
||||
'`VALIDATE_TYPE.WRANING` was a wrong typo, please use `VALIDATE_TYPE.WARNING` instead',
|
||||
);
|
||||
|
||||
const validateClx = classNames({
|
||||
'validate-error': validateMessage && validateMessage.type === VALIDATE_TYPE.ERROR,
|
||||
'validate-warning': validateMessage && ([VALIDATE_TYPE.WRANING, VALIDATE_TYPE.WARNING]).includes(validateMessage.type),
|
||||
'validate-info': validateMessage && validateMessage.type === VALIDATE_TYPE.INFO,
|
||||
});
|
||||
|
||||
const renderValidateMessage = () => {
|
||||
if (validateMessage && validateMessage.message) {
|
||||
return <div className={classNames('validate-message', validateClx, { popup })}>
|
||||
{validateMessage.message}
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const input: HTMLInputElement = event.target;
|
||||
let value;
|
||||
if (input.type === 'number') {
|
||||
value = event.target.valueAsNumber;
|
||||
} else {
|
||||
value = event.target.value;
|
||||
}
|
||||
if (typeof validate === 'function') {
|
||||
const message = validate(value);
|
||||
setValidateMessage(message);
|
||||
}
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(event);
|
||||
}
|
||||
if (typeof onValueChange === 'function') {
|
||||
onValueChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={classNames('input-box', { popup })}>
|
||||
<Input
|
||||
type='text'
|
||||
ref={ref}
|
||||
className={classNames(className, validateMessage, validateClx)}
|
||||
onChange={handleChange}
|
||||
{...restProps}
|
||||
/>
|
||||
{renderValidateMessage()}
|
||||
</div>;
|
||||
});
|
||||
);
|
||||
|
||||
ValidateInput.displayName = 'KTValidateInput';
|
||||
|
|
|
@ -2,8 +2,6 @@ import React from 'react';
|
|||
|
||||
import './loading.less';
|
||||
|
||||
export const Loading = React.memo(() => {
|
||||
return <div className='loading_indicator'/>;
|
||||
});
|
||||
export const Loading = React.memo(() => <div className='loading_indicator' />);
|
||||
|
||||
Loading.displayName = 'Loading';
|
||||
|
|
|
@ -22,9 +22,7 @@ LocalizeContext.displayName = 'LocalizeContext';
|
|||
export function LocalizeContextProvider(props: React.PropsWithChildren<{ value: ILocalizeContext }>) {
|
||||
return (
|
||||
<LocalizeContext.Provider value={props.value}>
|
||||
<LocalizeContext.Consumer>
|
||||
{(value) => props.value === value ? props.children : null}
|
||||
</LocalizeContext.Consumer>
|
||||
<LocalizeContext.Consumer>{(value) => (props.value === value ? props.children : null)}</LocalizeContext.Consumer>
|
||||
</LocalizeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -34,9 +32,7 @@ type IComponentContextProps<T extends string> = IiconContext<T> & ILocalizeConte
|
|||
export function ComponentContextProvider(props: React.PropsWithChildren<{ value: IComponentContextProps<any> }>) {
|
||||
return (
|
||||
<IconContextProvider value={props.value}>
|
||||
<LocalizeContextProvider value={{ localize: props.value.localize }}>
|
||||
{props.children}
|
||||
</LocalizeContextProvider>
|
||||
<LocalizeContextProvider value={{ localize: props.value.localize }}>{props.children}</LocalizeContextProvider>
|
||||
</IconContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,10 +4,7 @@ import { ClickParam } from '.';
|
|||
import MenuContext, { MenuContextProps } from './MenuContext';
|
||||
|
||||
export interface MenuItemProps
|
||||
extends Omit<
|
||||
React.HTMLAttributes<HTMLLIElement>,
|
||||
'title' | 'onClick' | 'onMouseEnter' | 'onMouseLeave'
|
||||
> {
|
||||
extends Omit<React.HTMLAttributes<HTMLLIElement>, 'title' | 'onClick' | 'onMouseEnter' | 'onMouseLeave'> {
|
||||
rootPrefixCls?: string;
|
||||
disabled?: boolean;
|
||||
level?: number;
|
||||
|
@ -27,11 +24,11 @@ export default class MenuItem extends React.Component<MenuItemProps> {
|
|||
|
||||
onKeyDown = (e: React.MouseEvent<HTMLElement>) => {
|
||||
this.menuItem.onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
saveMenuItem = (menuItem: this) => {
|
||||
this.menuItem = menuItem;
|
||||
}
|
||||
};
|
||||
|
||||
renderItem = () => {
|
||||
const { title, ...rest } = this.props;
|
||||
|
@ -47,7 +44,7 @@ export default class MenuItem extends React.Component<MenuItemProps> {
|
|||
}}
|
||||
</MenuContext.Consumer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.renderItem();
|
||||
|
|
|
@ -32,21 +32,15 @@ class SubMenu extends React.Component<SubMenuProps, any> {
|
|||
|
||||
onKeyDown = (e: React.MouseEvent<HTMLElement>) => {
|
||||
this.subMenu.onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
saveSubMenu = (subMenu: any) => {
|
||||
this.subMenu = subMenu;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { popupClassName } = this.props;
|
||||
return (
|
||||
<RcSubMenu
|
||||
{...this.props}
|
||||
ref={this.saveSubMenu}
|
||||
popupClassName={popupClassName}
|
||||
/>
|
||||
);
|
||||
return <RcSubMenu {...this.props} ref={this.saveSubMenu} popupClassName={popupClassName} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,17 +89,13 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
} else {
|
||||
// [Legacy] Old code will return after `openKeys` changed.
|
||||
// Not sure the reason, we should keep this logic still.
|
||||
if (
|
||||
(nextProps.inlineCollapsed && !prevProps.inlineCollapsed)
|
||||
) {
|
||||
if (nextProps.inlineCollapsed && !prevProps.inlineCollapsed) {
|
||||
newState.switchingModeFromInline = true;
|
||||
newState.inlineOpenKeys = prevState.openKeys;
|
||||
newState.openKeys = [];
|
||||
}
|
||||
|
||||
if (
|
||||
(!nextProps.inlineCollapsed && prevProps.inlineCollapsed)
|
||||
) {
|
||||
if (!nextProps.inlineCollapsed && prevProps.inlineCollapsed) {
|
||||
newState.openKeys = prevState.inlineOpenKeys;
|
||||
newState.inlineOpenKeys = [];
|
||||
}
|
||||
|
@ -163,9 +159,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
return inlineCollapsed;
|
||||
}
|
||||
|
||||
getOpenMotionProps(
|
||||
menuMode: MenuMode,
|
||||
): { openTransitionName?: any; openAnimation?: any; motion?: object } {
|
||||
getOpenMotionProps(menuMode: MenuMode): { openTransitionName?: any; openAnimation?: any; motion?: object } {
|
||||
const { openTransitionName, openAnimation, motion } = this.props;
|
||||
|
||||
// Provides by user
|
||||
|
@ -212,7 +206,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
if (onMouseEnter) {
|
||||
onMouseEnter(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleTransitionEnd = (e: TransitionEvent) => {
|
||||
// when inlineCollapsed menu width animation finished
|
||||
|
@ -224,9 +218,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
const { className } = e.target as HTMLElement | SVGElement;
|
||||
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during an animation.
|
||||
const classNameValue =
|
||||
Object.prototype.toString.call(className) === '[object SVGAnimatedString]'
|
||||
? className.animVal
|
||||
: className;
|
||||
Object.prototype.toString.call(className) === '[object SVGAnimatedString]' ? className.animVal : className;
|
||||
|
||||
// Fix for <Menu style={{ width: '100%' }} />, the width transition won't trigger when menu is collapsed
|
||||
// https://github.com/ant-design/ant-design-pro/issues/2783
|
||||
|
@ -234,7 +226,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
if (widthCollapsed || iconScaled) {
|
||||
this.restoreModeVerticalFromInline();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = (e: ClickParam) => {
|
||||
this.handleOpenChange([]);
|
||||
|
@ -243,7 +235,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleOpenChange = (openKeys: string[]) => {
|
||||
this.setOpenKeys(openKeys);
|
||||
|
@ -252,7 +244,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
if (onOpenChange) {
|
||||
onOpenChange(openKeys);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
restoreModeVerticalFromInline() {
|
||||
const { switchingModeFromInline } = this.state;
|
||||
|
@ -303,7 +295,7 @@ class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
|||
onMouseEnter={this.handleMouseEnter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -331,8 +323,6 @@ export class Menu extends React.Component<MenuProps, {}> {
|
|||
static ItemGroup = ItemGroup;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<InternalMenu {...this.props} />
|
||||
);
|
||||
return <InternalMenu {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,8 +157,8 @@
|
|||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s @ease-in-out, border-color 0.3s @ease-in-out,
|
||||
background 0.3s @ease-in-out, padding 0.15s @ease-in-out;
|
||||
transition: color 0.3s @ease-in-out, border-color 0.3s @ease-in-out, background 0.3s @ease-in-out,
|
||||
padding 0.15s @ease-in-out;
|
||||
.@{iconfont-css-prefix} {
|
||||
min-width: 14px;
|
||||
margin-right: 10px;
|
||||
|
@ -233,8 +233,7 @@
|
|||
background-image: linear-gradient(to right, @menu-item-color, @menu-item-color);
|
||||
background-image: ~'none \9';
|
||||
border-radius: 2px;
|
||||
transition: background 0.3s @ease-in-out, transform 0.3s @ease-in-out,
|
||||
top 0.3s @ease-in-out;
|
||||
transition: background 0.3s @ease-in-out, transform 0.3s @ease-in-out, top 0.3s @ease-in-out;
|
||||
content: '';
|
||||
}
|
||||
&::before {
|
||||
|
@ -262,9 +261,7 @@
|
|||
}
|
||||
|
||||
&-open {
|
||||
&.@{menu-prefix-cls}-submenu-inline
|
||||
> .@{menu-prefix-cls}-submenu-title
|
||||
.@{menu-prefix-cls}-submenu-arrow {
|
||||
&.@{menu-prefix-cls}-submenu-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow {
|
||||
transform: translateY(-2px);
|
||||
&::after {
|
||||
transform: rotate(-45deg) translateX(-2px);
|
||||
|
@ -404,9 +401,7 @@
|
|||
&-inline-collapsed {
|
||||
width: @menu-collapsed-width;
|
||||
> .@{menu-prefix-cls}-item,
|
||||
> .@{menu-prefix-cls}-item-group
|
||||
> .@{menu-prefix-cls}-item-group-list
|
||||
> .@{menu-prefix-cls}-item,
|
||||
> .@{menu-prefix-cls}-item-group > .@{menu-prefix-cls}-item-group-list > .@{menu-prefix-cls}-item,
|
||||
> .@{menu-prefix-cls}-item-group
|
||||
> .@{menu-prefix-cls}-item-group-list
|
||||
> .@{menu-prefix-cls}-submenu
|
||||
|
|
|
@ -3,11 +3,10 @@ import antdMessage from './message';
|
|||
import './style.less';
|
||||
|
||||
function generateSnackbar(funName: string) {
|
||||
return (content: string | React.ReactNode, duration?: number): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
return (content: string | React.ReactNode, duration?: number): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
antdMessage[funName](content, duration, resolve);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const message = {
|
||||
|
|
|
@ -83,11 +83,7 @@ function notice(args: ArgsProps): MessageType {
|
|||
duration,
|
||||
style: {},
|
||||
content: (
|
||||
<div
|
||||
className={`${prefixCls}-custom-content${
|
||||
args.type ? ` ${prefixCls}-${args.type}` : ''
|
||||
}`}
|
||||
>
|
||||
<div className={`${prefixCls}-custom-content${args.type ? ` ${prefixCls}-${args.type}` : ''}`}>
|
||||
{args.icon || (IconComponent && <IconComponent />)}
|
||||
<span>{args.content}</span>
|
||||
</div>
|
||||
|
@ -101,8 +97,7 @@ function notice(args: ArgsProps): MessageType {
|
|||
messageInstance.removeNotice(target);
|
||||
}
|
||||
};
|
||||
result.then = (filled: ThenableArgument, rejected: ThenableArgument) =>
|
||||
closePromise.then(filled, rejected);
|
||||
result.then = (filled: ThenableArgument, rejected: ThenableArgument) => closePromise.then(filled, rejected);
|
||||
result.promise = closePromise;
|
||||
return result;
|
||||
}
|
||||
|
@ -113,10 +108,7 @@ type JointContent = ConfigContent | ArgsProps;
|
|||
export type ConfigOnClose = () => void;
|
||||
|
||||
function isArgsProps(content: JointContent): content is ArgsProps {
|
||||
return (
|
||||
Object.prototype.toString.call(content) === '[object Object]' &&
|
||||
!!(content as ArgsProps).content
|
||||
);
|
||||
return Object.prototype.toString.call(content) === '[object Object]' && !!(content as ArgsProps).content;
|
||||
}
|
||||
|
||||
export interface ConfigOptions {
|
||||
|
|
|
@ -111,9 +111,7 @@ export interface ModalFuncProps {
|
|||
maskTransitionName?: string;
|
||||
}
|
||||
|
||||
export type ModalFunc = (
|
||||
props: ModalFuncProps,
|
||||
) => {
|
||||
export type ModalFunc = (props: ModalFuncProps) => {
|
||||
destroy: () => void;
|
||||
update: (newConfig: ModalFuncProps) => void;
|
||||
};
|
||||
|
@ -163,14 +161,14 @@ export default class Modal extends React.Component<ModalProps, {}> {
|
|||
if (onCancel) {
|
||||
onCancel(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleOk = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { onOk } = this.props;
|
||||
if (onOk) {
|
||||
onOk(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderFooter = (locale: ModalLocale) => {
|
||||
const { okText, okType, cancelText, confirmLoading } = this.props;
|
||||
|
@ -179,17 +177,12 @@ export default class Modal extends React.Component<ModalProps, {}> {
|
|||
<Button onClick={this.handleCancel} {...this.props.cancelButtonProps}>
|
||||
{cancelText || locale.cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
type={okType}
|
||||
loading={confirmLoading}
|
||||
onClick={this.handleOk}
|
||||
{...this.props.okButtonProps}
|
||||
>
|
||||
<Button type={okType} loading={confirmLoading} onClick={this.handleOk} {...this.props.okButtonProps}>
|
||||
{okText || locale.okText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderModal = () => {
|
||||
const {
|
||||
|
@ -225,7 +218,7 @@ export default class Modal extends React.Component<ModalProps, {}> {
|
|||
closeIcon={closeIconToRender}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.renderModal();
|
||||
|
|
|
@ -24,7 +24,7 @@ const cachedArgs: Map<string, [MessageType, ArgsProps]> = new Map();
|
|||
export function open<T = string>(
|
||||
message: string | React.ReactNode,
|
||||
type: MessageType,
|
||||
closable: boolean = true,
|
||||
closable = true,
|
||||
key: string,
|
||||
buttons?: string[],
|
||||
description?: string | React.ReactNode,
|
||||
|
@ -39,7 +39,7 @@ export function open<T = string>(
|
|||
['kt-notification-error']: type === MessageType.Error,
|
||||
['kt-notification-warn']: type === MessageType.Warning,
|
||||
}),
|
||||
duration: duration !== undefined ? null : (DURATION[type] / 1000),
|
||||
duration: duration !== undefined ? null : DURATION[type] / 1000,
|
||||
onClose: () => {
|
||||
onClose && onClose();
|
||||
cachedArgs.delete(key);
|
||||
|
@ -47,17 +47,19 @@ export function open<T = string>(
|
|||
},
|
||||
btn: buttons
|
||||
? buttons.map((button, index) => (
|
||||
<Button
|
||||
className={clx('kt-notification-button')}
|
||||
size='small'
|
||||
ghost={index === 0}
|
||||
onClick={() => {
|
||||
resolve(button as any);
|
||||
antdNotification.close(key);
|
||||
}}
|
||||
key={button}>
|
||||
{button}
|
||||
</Button>))
|
||||
<Button
|
||||
className={clx('kt-notification-button')}
|
||||
size='small'
|
||||
ghost={index === 0}
|
||||
onClick={() => {
|
||||
resolve(button as any);
|
||||
antdNotification.close(key);
|
||||
}}
|
||||
key={button}
|
||||
>
|
||||
{button}
|
||||
</Button>
|
||||
))
|
||||
: null,
|
||||
message,
|
||||
description,
|
||||
|
|
|
@ -198,7 +198,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.@{notification-prefix-cls}-wrapper {
|
||||
background: var(--notifications-background) !important;
|
||||
border-color: var(--notifications-border) !important;
|
||||
|
@ -212,7 +211,9 @@
|
|||
}
|
||||
|
||||
.@{notification-prefix-cls}-notice {
|
||||
&-close, &-message, &-description {
|
||||
&-close,
|
||||
&-message,
|
||||
&-description {
|
||||
color: var(--notifications-foreground) !important;
|
||||
font-size: 12px !important;
|
||||
margin-left: 22px !important;
|
||||
|
|
|
@ -49,11 +49,7 @@ function setNotificationConfig(options: ConfigProps) {
|
|||
}
|
||||
}
|
||||
|
||||
function getPlacementStyle(
|
||||
placement: NotificationPlacement,
|
||||
top: number = defaultTop,
|
||||
bottom: number = defaultBottom,
|
||||
) {
|
||||
function getPlacementStyle(placement: NotificationPlacement, top: number = defaultTop, bottom: number = defaultBottom) {
|
||||
let style;
|
||||
switch (placement) {
|
||||
case 'topLeft':
|
||||
|
@ -177,9 +173,7 @@ function notice(args: ArgsProps) {
|
|||
}
|
||||
|
||||
const autoMarginTag =
|
||||
!args.description && iconNode ? (
|
||||
<span className={`${prefixCls}-message-single-line-auto-margin`} />
|
||||
) : null;
|
||||
!args.description && iconNode ? <span className={`${prefixCls}-message-single-line-auto-margin`} /> : null;
|
||||
|
||||
const { placement, top, bottom, getContainer, closeIcon } = args;
|
||||
|
||||
|
@ -220,9 +214,7 @@ function notice(args: ArgsProps) {
|
|||
const api: any = {
|
||||
open: notice,
|
||||
close(key: string) {
|
||||
Object.keys(notificationInstance).forEach((cacheKey) =>
|
||||
notificationInstance[cacheKey].removeNotice(key),
|
||||
);
|
||||
Object.keys(notificationInstance).forEach((cacheKey) => notificationInstance[cacheKey].removeNotice(key));
|
||||
},
|
||||
config: setNotificationConfig,
|
||||
destroy() {
|
||||
|
|
|
@ -19,19 +19,27 @@ export interface IOverlayProps {
|
|||
getContainer?: string | HTMLElement | getContainerFunc | false | null;
|
||||
}
|
||||
|
||||
export const Overlay: React.FC<IOverlayProps> = (({ maskClosable = false, closable = true, className, onClose, children, footer, title, getContainer, ...restProps }) => {
|
||||
return (
|
||||
<Modal
|
||||
footer={footer ? footer : null}
|
||||
maskClosable={maskClosable}
|
||||
closable={closable}
|
||||
onCancel={onClose}
|
||||
title={title}
|
||||
getContainer={getContainer}
|
||||
className={clsx('kt-overlay', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
export const Overlay: React.FC<IOverlayProps> = ({
|
||||
maskClosable = false,
|
||||
closable = true,
|
||||
className,
|
||||
onClose,
|
||||
children,
|
||||
footer,
|
||||
title,
|
||||
getContainer,
|
||||
...restProps
|
||||
}) => (
|
||||
<Modal
|
||||
footer={footer ? footer : null}
|
||||
maskClosable={maskClosable}
|
||||
closable={closable}
|
||||
onCancel={onClose}
|
||||
title={title}
|
||||
getContainer={getContainer}
|
||||
className={clsx('kt-overlay', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -40,5 +40,4 @@
|
|||
color: var(--kt-notificationsCloseIcon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export const Popover: React.FC<{
|
|||
insertClass?: string;
|
||||
content?: React.ReactElement;
|
||||
trigger?: PopoverTriggerType;
|
||||
display?: boolean, // 使用程序控制的是否显示
|
||||
display?: boolean; // 使用程序控制的是否显示
|
||||
[key: string]: any;
|
||||
popoverClass?: string;
|
||||
position?: PopoverPosition;
|
||||
|
@ -34,7 +34,20 @@ export const Popover: React.FC<{
|
|||
action?: string;
|
||||
onClickAction?: (args: any) => void;
|
||||
}> = ({
|
||||
delay, children, trigger, display, id, insertClass, popoverClass, content, position = PopoverPosition.top, title, titleClassName, action, onClickAction, ...restProps
|
||||
delay,
|
||||
children,
|
||||
trigger,
|
||||
display,
|
||||
id,
|
||||
insertClass,
|
||||
popoverClass,
|
||||
content,
|
||||
position = PopoverPosition.top,
|
||||
title,
|
||||
titleClassName,
|
||||
action,
|
||||
onClickAction,
|
||||
...restProps
|
||||
}) => {
|
||||
const childEl = React.useRef<HTMLSpanElement | null>(null);
|
||||
const contentEl = React.useRef<HTMLDivElement | null>(null);
|
||||
|
@ -77,25 +90,25 @@ export const Popover: React.FC<{
|
|||
if (position === PopoverPosition.top) {
|
||||
const contentLeft = left - contentRect.width / 2 + width / 2;
|
||||
const contentTop = top - contentRect.height - 7;
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.top = (contentTop < 0 ? 0 : contentTop) + 'px';
|
||||
contentEl.current.style.visibility = 'visible';
|
||||
} else if (position === PopoverPosition.bottom) {
|
||||
const contentLeft = left - contentRect.width / 2 + width / 2;
|
||||
const contentTop = top + height + 7;
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current!.style.top = (contentTop < 0 ? 0 : contentTop) + 'px';
|
||||
contentEl.current.style.visibility = 'visible';
|
||||
} else if (position === PopoverPosition.left) {
|
||||
const contentLeft = left - contentRect.width - 7;
|
||||
const contentTop = top - contentRect.height / 2 + height / 2;
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.top = (contentTop < 0 ? 0 : contentTop) + 'px';
|
||||
contentEl.current.style.visibility = 'visible';
|
||||
} else if (position === PopoverPosition.right) {
|
||||
const contentLeft = left + width + 7;
|
||||
const contentTop = top - contentRect.height / 2 + height / 2;
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.left = (contentLeft < 0 ? 0 : contentLeft) + 'px';
|
||||
contentEl.current.style.top = (contentTop < 0 ? 0 : contentTop) + 'px';
|
||||
contentEl.current.style.visibility = 'visible';
|
||||
}
|
||||
|
@ -132,27 +145,27 @@ export const Popover: React.FC<{
|
|||
};
|
||||
}, [display]);
|
||||
|
||||
return(
|
||||
return (
|
||||
<div
|
||||
{...Object.assign({}, restProps)}
|
||||
className={clx('kt-popover', insertClass)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div
|
||||
className={clx(popoverClass || '', 'kt-popover-content', `kt-popover-${position}`)}
|
||||
ref={contentEl}
|
||||
id={id}
|
||||
>
|
||||
{title && <p className={clx('kt-popover-title', titleClassName)}>
|
||||
{title}
|
||||
{action && <Button size='small' type='link' onClick={onClickAction || noop}>{action}</Button>}
|
||||
</p>}
|
||||
<div className={clx(popoverClass || '', 'kt-popover-content', `kt-popover-${position}`)} ref={contentEl} id={id}>
|
||||
{title && (
|
||||
<p className={clx('kt-popover-title', titleClassName)}>
|
||||
{title}
|
||||
{action && (
|
||||
<Button size='small' type='link' onClick={onClickAction || noop}>
|
||||
{action}
|
||||
</Button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{content || ''}
|
||||
</div>
|
||||
<span ref={childEl}>
|
||||
{children}
|
||||
</span>
|
||||
<span ref={childEl}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -103,7 +103,8 @@
|
|||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.kt-button, .small-button-size {
|
||||
&.kt-button,
|
||||
.small-button-size {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
@ -127,9 +128,9 @@
|
|||
.arrow('bottom');
|
||||
}
|
||||
&-left {
|
||||
.arrow('left')
|
||||
.arrow('left');
|
||||
}
|
||||
&-right {
|
||||
.arrow('right')
|
||||
.arrow('right');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,13 +105,25 @@ export interface IRecycleListHandler {
|
|||
scrollToIndex: (index: number, position?: Align) => void;
|
||||
}
|
||||
|
||||
export const RECYCLE_LIST_STABILIZATION_TIME: number = 500;
|
||||
export const RECYCLE_LIST_OVER_SCAN_COUNT: number = 50;
|
||||
export const RECYCLE_LIST_STABILIZATION_TIME = 500;
|
||||
export const RECYCLE_LIST_OVER_SCAN_COUNT = 50;
|
||||
|
||||
export const RecycleList: React.FC<IRecycleListProps> = ({
|
||||
width, height, maxHeight, minHeight, className, style, data, onReady, itemHeight, header: Header, footer: Footer, template: Template, paddingBottomSize, getSize: customGetSize,
|
||||
width,
|
||||
height,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
className,
|
||||
style,
|
||||
data,
|
||||
onReady,
|
||||
itemHeight,
|
||||
header: Header,
|
||||
footer: Footer,
|
||||
template: Template,
|
||||
paddingBottomSize,
|
||||
getSize: customGetSize,
|
||||
}) => {
|
||||
|
||||
const listRef = React.useRef<FixedSizeList | VariableSizeList>();
|
||||
const sizeMap = React.useRef<{ [key: string]: number }>({});
|
||||
const scrollToIndexTimer = React.useRef<any>();
|
||||
|
@ -125,7 +137,7 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
// custom alignment: center, start, or end
|
||||
scrollToIndex: (index: number, position: Align = 'start') => {
|
||||
let locationIndex = index;
|
||||
if (!!Header) {
|
||||
if (Header) {
|
||||
locationIndex++;
|
||||
}
|
||||
if (typeof itemHeight === 'number') {
|
||||
|
@ -163,12 +175,15 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const getSize = React.useCallback((index: string | number) => {
|
||||
if (customGetSize) {
|
||||
return customGetSize(Number(index));
|
||||
}
|
||||
return (sizeMap?.current || [])[index] || itemHeight || 100;
|
||||
}, [itemHeight, customGetSize]);
|
||||
const getSize = React.useCallback(
|
||||
(index: string | number) => {
|
||||
if (customGetSize) {
|
||||
return customGetSize(Number(index));
|
||||
}
|
||||
return (sizeMap?.current || [])[index] || itemHeight || 100;
|
||||
},
|
||||
[itemHeight, customGetSize],
|
||||
);
|
||||
|
||||
const getMaxListHeight = React.useCallback(() => {
|
||||
if (maxHeight) {
|
||||
|
@ -192,10 +207,10 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
|
||||
const adjustedRowCount = React.useMemo(() => {
|
||||
let count = data.length;
|
||||
if (!!Header) {
|
||||
if (Header) {
|
||||
count++;
|
||||
}
|
||||
if (!!Footer) {
|
||||
if (Footer) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
|
@ -205,19 +220,23 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
let node;
|
||||
if (index === 0) {
|
||||
if (Header) {
|
||||
return <div style={style}>
|
||||
<Header />
|
||||
</div>;
|
||||
return (
|
||||
<div style={style}>
|
||||
<Header />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if ((index + 1) === adjustedRowCount) {
|
||||
if (!!Footer) {
|
||||
return <div style={style}>
|
||||
<Footer />
|
||||
</div>;
|
||||
if (index + 1 === adjustedRowCount) {
|
||||
if (Footer) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!!Header) {
|
||||
if (Header) {
|
||||
node = data[index - 1];
|
||||
} else {
|
||||
node = data[index];
|
||||
|
@ -232,9 +251,11 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
'aria-posinset': index,
|
||||
};
|
||||
|
||||
return <div style={style} role='listitem' {...ariaInfo}>
|
||||
<Template data={node} index={index} />
|
||||
</div>;
|
||||
return (
|
||||
<div style={style} role='listitem' {...ariaInfo}>
|
||||
<Template data={node} index={index} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDynamicItem = ({ index, style }): JSX.Element => {
|
||||
|
@ -243,7 +264,7 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
const setItemSize = () => {
|
||||
if (rowRoot.current) {
|
||||
let height = 0;
|
||||
// tslint:disable-next-line:prefer-for-of
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
for (let i = 0; i < rowRoot.current.children.length; i++) {
|
||||
height += rowRoot.current.children[i].getBoundingClientRect().height;
|
||||
}
|
||||
|
@ -275,19 +296,23 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
let node;
|
||||
if (index === 0) {
|
||||
if (Header) {
|
||||
return <div style={style}>
|
||||
<Header />
|
||||
</div>;
|
||||
return (
|
||||
<div style={style}>
|
||||
<Header />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if ((index + 1) === adjustedRowCount) {
|
||||
if (!!Footer) {
|
||||
return <div style={style}>
|
||||
<Footer />
|
||||
</div>;
|
||||
if (index + 1 === adjustedRowCount) {
|
||||
if (Footer) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!!Header) {
|
||||
if (Header) {
|
||||
node = data[index - 1];
|
||||
} else {
|
||||
node = data[index];
|
||||
|
@ -296,9 +321,11 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
return <div style={style}></div>;
|
||||
}
|
||||
|
||||
return <div style={style} ref={rowRoot}>
|
||||
<Template data={node} index={index} />
|
||||
</div>;
|
||||
return (
|
||||
<div style={style} ref={rowRoot}>
|
||||
<Template data={node} index={index} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getItemKey = (index: number) => {
|
||||
|
@ -319,14 +346,16 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
// 为 List 添加下边距
|
||||
const InnerElementType = React.forwardRef((props, ref) => {
|
||||
const { style, ...rest } = props as any;
|
||||
return <div
|
||||
ref={ref!}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style.height) + (paddingBottomSize ? paddingBottomSize : 0)}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>;
|
||||
return (
|
||||
<div
|
||||
ref={ref!}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style.height) + (paddingBottomSize ? paddingBottomSize : 0)}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
|
@ -351,55 +380,59 @@ export const RecycleList: React.FC<IRecycleListProps> = ({
|
|||
currentHeight = maxH;
|
||||
}
|
||||
if (isDynamicList) {
|
||||
return <List
|
||||
width={width}
|
||||
height={currentHeight}
|
||||
// 这里的数据不是必要的,主要用于在每次更新列表
|
||||
itemData={[]}
|
||||
itemSize={getSize}
|
||||
itemCount={adjustedRowCount}
|
||||
getItemKey={getItemKey}
|
||||
overscanCount={RECYCLE_LIST_OVER_SCAN_COUNT}
|
||||
ref={listRef}
|
||||
style={{
|
||||
transform: 'translate3d(0px, 0px, 0px)',
|
||||
...style,
|
||||
}}
|
||||
className={cls(className, 'kt-recycle-list')}
|
||||
innerElementType={InnerElementType}
|
||||
outerElementType={ScrollbarsVirtualList}
|
||||
estimatedItemSize={calcEstimatedSize}>
|
||||
{renderDynamicItem}
|
||||
</List>;
|
||||
return (
|
||||
<List
|
||||
width={width}
|
||||
height={currentHeight}
|
||||
// 这里的数据不是必要的,主要用于在每次更新列表
|
||||
itemData={[]}
|
||||
itemSize={getSize}
|
||||
itemCount={adjustedRowCount}
|
||||
getItemKey={getItemKey}
|
||||
overscanCount={RECYCLE_LIST_OVER_SCAN_COUNT}
|
||||
ref={listRef}
|
||||
style={{
|
||||
transform: 'translate3d(0px, 0px, 0px)',
|
||||
...style,
|
||||
}}
|
||||
className={cls(className, 'kt-recycle-list')}
|
||||
innerElementType={InnerElementType}
|
||||
outerElementType={ScrollbarsVirtualList}
|
||||
estimatedItemSize={calcEstimatedSize}
|
||||
>
|
||||
{renderDynamicItem}
|
||||
</List>
|
||||
);
|
||||
} else {
|
||||
return <List
|
||||
width={width}
|
||||
height={currentHeight}
|
||||
// 这里的数据不是必要的,主要用于在每次更新列表
|
||||
itemData={[]}
|
||||
itemSize={itemHeight}
|
||||
itemCount={adjustedRowCount}
|
||||
getItemKey={getItemKey}
|
||||
overscanCount={RECYCLE_LIST_OVER_SCAN_COUNT}
|
||||
ref={listRef}
|
||||
style={{
|
||||
transform: 'translate3d(0px, 0px, 0px)',
|
||||
...style,
|
||||
}}
|
||||
className={cls(className, 'kt-recycle-list')}
|
||||
innerElementType={InnerElementType}
|
||||
outerElementType={ScrollbarsVirtualList}>
|
||||
{renderItem}
|
||||
</List>;
|
||||
return (
|
||||
<List
|
||||
width={width}
|
||||
height={currentHeight}
|
||||
// 这里的数据不是必要的,主要用于在每次更新列表
|
||||
itemData={[]}
|
||||
itemSize={itemHeight}
|
||||
itemCount={adjustedRowCount}
|
||||
getItemKey={getItemKey}
|
||||
overscanCount={RECYCLE_LIST_OVER_SCAN_COUNT}
|
||||
ref={listRef}
|
||||
style={{
|
||||
transform: 'translate3d(0px, 0px, 0px)',
|
||||
...style,
|
||||
}}
|
||||
className={cls(className, 'kt-recycle-list')}
|
||||
innerElementType={InnerElementType}
|
||||
outerElementType={ScrollbarsVirtualList}
|
||||
>
|
||||
{renderItem}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAutoSizeList) {
|
||||
return renderContent({ width, height });
|
||||
} else {
|
||||
return <AutoSizer>
|
||||
{renderContent}
|
||||
</AutoSizer>;
|
||||
return <AutoSizer>{renderContent}</AutoSizer>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -149,7 +149,10 @@ export interface IRecycleTreeHandle {
|
|||
*
|
||||
* @param pathOrTreeNode 节点或者节点路径
|
||||
*/
|
||||
promptRename(pathOrTreeNode: string | TreeNode | CompositeTreeNode, defaultName?: string): Promise<RenamePromptHandle>;
|
||||
promptRename(
|
||||
pathOrTreeNode: string | TreeNode | CompositeTreeNode,
|
||||
defaultName?: string,
|
||||
): Promise<RenamePromptHandle>;
|
||||
/**
|
||||
* 展开节点
|
||||
*
|
||||
|
@ -169,7 +172,11 @@ export interface IRecycleTreeHandle {
|
|||
* @param align IRecycleTreeAlign
|
||||
* @param untilStable 是否在节点稳定时再进行定位操作,部分Tree可能在定位过程中会有不断传入的变化,如文件树
|
||||
*/
|
||||
ensureVisible(pathOrTreeNode: string | TreeNode | CompositeTreeNode, align?: IRecycleTreeAlign, untilStable?: boolean): Promise<TreeNode | undefined>;
|
||||
ensureVisible(
|
||||
pathOrTreeNode: string | TreeNode | CompositeTreeNode,
|
||||
align?: IRecycleTreeAlign,
|
||||
untilStable?: boolean,
|
||||
): Promise<TreeNode | undefined>;
|
||||
/**
|
||||
* 获取当前Tree的宽高信息
|
||||
*
|
||||
|
@ -223,27 +230,27 @@ interface IFilterNodeRendererProps {
|
|||
|
||||
const InnerElementType = React.forwardRef((props, ref) => {
|
||||
const { style, ...rest } = props as any;
|
||||
return <div
|
||||
ref={ref!}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style.height) + RecycleTree.PADDING_BOTTOM_SIZE}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>;
|
||||
return (
|
||||
<div
|
||||
ref={ref!}
|
||||
style={{
|
||||
...style,
|
||||
height: `${parseFloat(style.height) + RecycleTree.PADDING_BOTTOM_SIZE}px`,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
||||
public static PADDING_BOTTOM_SIZE: number = 22;
|
||||
private static DEFAULT_ITEM_HEIGHT: number = 22;
|
||||
private static BATCHED_UPDATE_MAX_DEBOUNCE_MS: number = 100;
|
||||
private static TRY_ENSURE_VISIBLE_MAX_TIMES: number = 5;
|
||||
public static PADDING_BOTTOM_SIZE = 22;
|
||||
private static DEFAULT_ITEM_HEIGHT = 22;
|
||||
private static BATCHED_UPDATE_MAX_DEBOUNCE_MS = 100;
|
||||
private static TRY_ENSURE_VISIBLE_MAX_TIMES = 5;
|
||||
private static FILTER_FUZZY_OPTIONS = {
|
||||
pre: '<match>',
|
||||
post: '</match>',
|
||||
extract: (node: TreeNode) => {
|
||||
return node?.name || '';
|
||||
},
|
||||
extract: (node: TreeNode) => node?.name || '',
|
||||
};
|
||||
private static DEFAULT_OVER_SCAN_COUNT = 50;
|
||||
|
||||
|
@ -258,7 +265,7 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
private onDidUpdateEmitter: Emitter<void> = new Emitter();
|
||||
private onDidModelChangeEmitter: Emitter<IModelChange> = new Emitter();
|
||||
// 索引应该比目标折叠节点索引+1,即位于折叠节点下的首个节点
|
||||
private newPromptInsertionIndex: number = -1;
|
||||
private newPromptInsertionIndex = -1;
|
||||
// 目标索引
|
||||
private promptTargetID: number;
|
||||
// 尝试定位次数
|
||||
|
@ -277,8 +284,9 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
let timer: number;
|
||||
const commitUpdate = () => {
|
||||
const { root } = this.props.model;
|
||||
let newFilePromptInsertionIndex: number = -1;
|
||||
if (this.promptTargetID > -1 &&
|
||||
let newFilePromptInsertionIndex = -1;
|
||||
if (
|
||||
this.promptTargetID > -1 &&
|
||||
this.promptHandle instanceof NewPromptHandle &&
|
||||
this.promptHandle.parent &&
|
||||
this.promptHandle.parent.expanded &&
|
||||
|
@ -319,7 +327,7 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
};
|
||||
return () => {
|
||||
if (!this.batchUpdatePromise) {
|
||||
this.batchUpdatePromise = new Promise((res) => this.batchUpdateResolver = res);
|
||||
this.batchUpdatePromise = new Promise((res) => (this.batchUpdateResolver = res));
|
||||
this.batchUpdatePromise.then(() => {
|
||||
this.batchUpdatePromise = null;
|
||||
this.batchUpdateResolver = null;
|
||||
|
@ -371,9 +379,11 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const { model } = this.props;
|
||||
this.listRef.current?.scrollTo(model.state.scrollOffset);
|
||||
this.disposables.push(model.onChange(this.batchUpdate));
|
||||
this.disposables.push(model.state.onDidLoadState(() => {
|
||||
this.listRef.current?.scrollTo(model.state.scrollOffset);
|
||||
}));
|
||||
this.disposables.push(
|
||||
model.state.onDidLoadState(() => {
|
||||
this.listRef.current?.scrollTo(model.state.scrollOffset);
|
||||
}),
|
||||
);
|
||||
this.onDidModelChangeEmitter.fire({
|
||||
preModel: prevProps.model,
|
||||
nextModel: model,
|
||||
|
@ -381,14 +391,17 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
}
|
||||
|
||||
private async promptNew(pathOrTreeNode: string | CompositeTreeNode, type: TreeNodeType = TreeNodeType.TreeNode): Promise<NewPromptHandle> {
|
||||
private async promptNew(
|
||||
pathOrTreeNode: string | CompositeTreeNode,
|
||||
type: TreeNodeType = TreeNodeType.TreeNode,
|
||||
): Promise<NewPromptHandle> {
|
||||
const { root } = this.props.model;
|
||||
let node = typeof pathOrTreeNode === 'string'
|
||||
? await root.getTreeNodeByPath(pathOrTreeNode)
|
||||
: pathOrTreeNode;
|
||||
let node = typeof pathOrTreeNode === 'string' ? await root.getTreeNodeByPath(pathOrTreeNode) : pathOrTreeNode;
|
||||
|
||||
if (type !== TreeNodeType.TreeNode && type !== TreeNodeType.CompositeTreeNode) {
|
||||
throw new TypeError(`Invalid type supplied. Expected 'TreeNodeType.TreeNode' or 'TreeNodeType.CompositeTreeNode', got ${type}`);
|
||||
throw new TypeError(
|
||||
`Invalid type supplied. Expected 'TreeNodeType.TreeNode' or 'TreeNodeType.CompositeTreeNode', got ${type}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!!node && !CompositeTreeNode.is(node)) {
|
||||
|
@ -404,7 +417,10 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const promptHandle = new NewPromptHandle(type, node as CompositeTreeNode);
|
||||
this.promptHandle = promptHandle;
|
||||
this.promptTargetID = node!.id;
|
||||
if (node !== root && (!(node as CompositeTreeNode).expanded || !root.isItemVisibleAtSurface(node as CompositeTreeNode))) {
|
||||
if (
|
||||
node !== root &&
|
||||
(!(node as CompositeTreeNode).expanded || !root.isItemVisibleAtSurface(node as CompositeTreeNode))
|
||||
) {
|
||||
// 调用setExpanded即会在之后调用batchUpdate函数
|
||||
await (node as CompositeTreeNode).setExpanded(true);
|
||||
} else {
|
||||
|
@ -421,19 +437,20 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
|
||||
// 使用箭头函数绑定当前this
|
||||
private promptNewTreeNode = (pathOrTreeNode: string | CompositeTreeNode): Promise<NewPromptHandle> => {
|
||||
return this.promptNew(pathOrTreeNode);
|
||||
}
|
||||
private promptNewTreeNode = (pathOrTreeNode: string | CompositeTreeNode): Promise<NewPromptHandle> =>
|
||||
this.promptNew(pathOrTreeNode);
|
||||
|
||||
private promptNewCompositeTreeNode = (pathOrTreeNode: string | CompositeTreeNode): Promise<NewPromptHandle> => {
|
||||
return this.promptNew(pathOrTreeNode, TreeNodeType.CompositeTreeNode);
|
||||
}
|
||||
private promptNewCompositeTreeNode = (pathOrTreeNode: string | CompositeTreeNode): Promise<NewPromptHandle> =>
|
||||
this.promptNew(pathOrTreeNode, TreeNodeType.CompositeTreeNode);
|
||||
|
||||
private promptRename = async (pathOrTreeNode: string | TreeNode, defaultName?: string): Promise<RenamePromptHandle> => {
|
||||
private promptRename = async (
|
||||
pathOrTreeNode: string | TreeNode,
|
||||
defaultName?: string,
|
||||
): Promise<RenamePromptHandle> => {
|
||||
const { root } = this.props.model;
|
||||
const node = (typeof pathOrTreeNode === 'string'
|
||||
? await root.getTreeNodeByPath(pathOrTreeNode)
|
||||
: pathOrTreeNode) as (TreeNode | CompositeTreeNode);
|
||||
const node = (
|
||||
typeof pathOrTreeNode === 'string' ? await root.getTreeNodeByPath(pathOrTreeNode) : pathOrTreeNode
|
||||
) as TreeNode | CompositeTreeNode;
|
||||
|
||||
if (!TreeNode.is(node) || CompositeTreeNode.isRoot(node)) {
|
||||
throw new TypeError(`Cannot rename object of type ${typeof node}`);
|
||||
|
@ -448,41 +465,46 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
this.listRef.current?.scrollToItem(root.getIndexAtTreeNodeId(this.promptTargetID));
|
||||
return this.promptHandle as RenamePromptHandle;
|
||||
}
|
||||
};
|
||||
|
||||
private expandNode = async (pathOrCompositeTreeNode: string | CompositeTreeNode) => {
|
||||
const { root } = this.props.model;
|
||||
const directory = typeof pathOrCompositeTreeNode === 'string'
|
||||
? await root.getTreeNodeByPath(pathOrCompositeTreeNode) as CompositeTreeNode
|
||||
: await root.getTreeNodeByPath(pathOrCompositeTreeNode.path);
|
||||
const directory =
|
||||
typeof pathOrCompositeTreeNode === 'string'
|
||||
? ((await root.getTreeNodeByPath(pathOrCompositeTreeNode)) as CompositeTreeNode)
|
||||
: await root.getTreeNodeByPath(pathOrCompositeTreeNode.path);
|
||||
|
||||
if (directory && CompositeTreeNode.is(directory)) {
|
||||
return (directory as CompositeTreeNode).setExpanded(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private collapseNode = async (pathOrCompositeTreeNode: string | CompositeTreeNode) => {
|
||||
const { root } = this.props.model;
|
||||
const directory = typeof pathOrCompositeTreeNode === 'string'
|
||||
? await root.getTreeNodeByPath(pathOrCompositeTreeNode) as CompositeTreeNode
|
||||
: root.getTreeNodeByPath(pathOrCompositeTreeNode.path);
|
||||
const directory =
|
||||
typeof pathOrCompositeTreeNode === 'string'
|
||||
? ((await root.getTreeNodeByPath(pathOrCompositeTreeNode)) as CompositeTreeNode)
|
||||
: root.getTreeNodeByPath(pathOrCompositeTreeNode.path);
|
||||
|
||||
if (directory && CompositeTreeNode.is(directory)) {
|
||||
return (directory as CompositeTreeNode).setCollapsed();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _isEnsuring: boolean = false;
|
||||
private ensureVisible = async (pathOrTreeNode: string | TreeNode | CompositeTreeNode, align: IRecycleTreeAlign = 'smart', untilStable: boolean = false): Promise<TreeNode | undefined> => {
|
||||
private _isEnsuring = false;
|
||||
private ensureVisible = async (
|
||||
pathOrTreeNode: string | TreeNode | CompositeTreeNode,
|
||||
align: IRecycleTreeAlign = 'smart',
|
||||
untilStable = false,
|
||||
): Promise<TreeNode | undefined> => {
|
||||
if (this._isEnsuring) {
|
||||
// 同一时间段只能让一次定位节点的操作生效
|
||||
return;
|
||||
}
|
||||
this._isEnsuring = true;
|
||||
const { root } = this.props.model;
|
||||
const node = typeof pathOrTreeNode === 'string'
|
||||
? await root.forceLoadTreeNodeAtPath(pathOrTreeNode)
|
||||
: pathOrTreeNode;
|
||||
const node =
|
||||
typeof pathOrTreeNode === 'string' ? await root.forceLoadTreeNodeAtPath(pathOrTreeNode) : pathOrTreeNode;
|
||||
if (!TreeNode.is(node) || CompositeTreeNode.isRoot(node)) {
|
||||
// 异常
|
||||
return;
|
||||
|
@ -501,7 +523,7 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
this._isEnsuring = false;
|
||||
return node as TreeNode;
|
||||
}
|
||||
};
|
||||
|
||||
private tryScrollIntoView(node: TreeNode | CompositeTreeNode | PromptHandle, align: IRecycleTreeAlign = 'auto') {
|
||||
const { root } = this.props.model;
|
||||
|
@ -512,7 +534,10 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
}
|
||||
|
||||
private tryScrollIntoViewWhileStable(node: TreeNode | CompositeTreeNode | PromptHandle, align: IRecycleTreeAlign = 'auto') {
|
||||
private tryScrollIntoViewWhileStable(
|
||||
node: TreeNode | CompositeTreeNode | PromptHandle,
|
||||
align: IRecycleTreeAlign = 'auto',
|
||||
) {
|
||||
const { root } = this.props.model;
|
||||
if (this.tryEnsureVisibleTimes > RecycleTree.TRY_ENSURE_VISIBLE_MAX_TIMES) {
|
||||
this.tryEnsureVisibleTimes = 0;
|
||||
|
@ -536,9 +561,11 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const { model, onReady } = this.props;
|
||||
this.listRef.current?.scrollTo(model.state.scrollOffset);
|
||||
this.disposables.push(model.onChange(this.batchUpdate));
|
||||
this.disposables.push(model.state.onDidLoadState(() => {
|
||||
this.listRef.current?.scrollTo(model.state.scrollOffset);
|
||||
}));
|
||||
this.disposables.push(
|
||||
model.state.onDidLoadState(() => {
|
||||
this.listRef.current?.scrollTo(model.state.scrollOffset);
|
||||
}),
|
||||
);
|
||||
if (typeof onReady === 'function') {
|
||||
const api: IRecycleTreeHandle = {
|
||||
promptNewTreeNode: this.promptNewTreeNode,
|
||||
|
@ -567,7 +594,9 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
|
||||
private set promptHandle(handle: NewPromptHandle | RenamePromptHandle) {
|
||||
if (this._promptHandle === handle) { return; }
|
||||
if (this._promptHandle === handle) {
|
||||
return;
|
||||
}
|
||||
if (this._promptHandle instanceof PromptHandle && !this._promptHandle.destroyed) {
|
||||
this._promptHandle.destroy();
|
||||
}
|
||||
|
@ -589,9 +618,12 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const promptInsertionIdx = this.newPromptInsertionIndex;
|
||||
const { root } = this.props.model;
|
||||
// 存在新建输入框
|
||||
if (promptInsertionIdx > -1 &&
|
||||
this.promptHandle && this.promptHandle.constructor === NewPromptHandle &&
|
||||
!this.promptHandle.destroyed) {
|
||||
if (
|
||||
promptInsertionIdx > -1 &&
|
||||
this.promptHandle &&
|
||||
this.promptHandle.constructor === NewPromptHandle &&
|
||||
!this.promptHandle.destroyed
|
||||
) {
|
||||
if (index === promptInsertionIdx) {
|
||||
cached = {
|
||||
itemType: TreeNodeType.NewPrompt,
|
||||
|
@ -607,7 +639,8 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
} else {
|
||||
const item = root.getTreeNodeAtIndex(index);
|
||||
// 检查是否为重命名节点
|
||||
if (item &&
|
||||
if (
|
||||
item &&
|
||||
item.id === this.promptTargetID &&
|
||||
this.promptHandle &&
|
||||
this.promptHandle.constructor === RenamePromptHandle &&
|
||||
|
@ -627,24 +660,24 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
this.idxToRendererPropsCache.set(index, cached!);
|
||||
}
|
||||
return cached!;
|
||||
}
|
||||
};
|
||||
|
||||
private handleListScroll = ({ scrollOffset }) => {
|
||||
const { model } = this.props;
|
||||
model.state.saveScrollOffset(scrollOffset);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据是否携带新建输入框计算行数
|
||||
private get adjustedRowCount() {
|
||||
const { root } = this.props.model;
|
||||
const { filter } = this.props;
|
||||
if (!!filter) {
|
||||
if (filter) {
|
||||
return this.filterFlattenBranch.length;
|
||||
}
|
||||
return (
|
||||
this.newPromptInsertionIndex > -1 &&
|
||||
this.promptHandle && this.promptHandle.constructor === NewPromptHandle &&
|
||||
!this.promptHandle.destroyed)
|
||||
return this.newPromptInsertionIndex > -1 &&
|
||||
this.promptHandle &&
|
||||
this.promptHandle.constructor === NewPromptHandle &&
|
||||
!this.promptHandle.destroyed
|
||||
? root.branchSize + 1
|
||||
: root.branchSize;
|
||||
}
|
||||
|
@ -654,11 +687,14 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const { getItemKey } = this.props;
|
||||
const id = getItemKey ? getItemKey(node) : undefined;
|
||||
return id ?? index;
|
||||
}
|
||||
};
|
||||
|
||||
// 过滤Root节点展示
|
||||
private filterItems = (filter: string) => {
|
||||
const { model: { root }, filterProvider } = this.props;
|
||||
const {
|
||||
model: { root },
|
||||
filterProvider,
|
||||
} = this.props;
|
||||
this.filterWatcherDisposeCollection.dispose();
|
||||
this.idToFilterRendererPropsCache.clear();
|
||||
if (!filter) {
|
||||
|
@ -696,14 +732,17 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const node = (item as any).original as TreeNode;
|
||||
idSets.add(node.id);
|
||||
let parent = node.parent;
|
||||
idToRenderTemplate.set(node.id, () => {
|
||||
return <div style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}} dangerouslySetInnerHTML={{ __html: item.string || '' }}></div>;
|
||||
});
|
||||
idToRenderTemplate.set(node.id, () => (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.string || '' }}
|
||||
></div>
|
||||
));
|
||||
// 不应包含根节点
|
||||
while (parent && !CompositeTreeNode.isRoot(parent)) {
|
||||
idSets.add(parent.id);
|
||||
|
@ -734,32 +773,36 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
}
|
||||
}
|
||||
// 根据折叠情况变化裁剪filterFlattenBranch
|
||||
this.filterWatcherDisposeCollection.push(root.watcher.on(TreeNodeEvent.DidChangeExpansionState, (target, nowExpanded) => {
|
||||
const expandItemIndex = this.filterFlattenBranch.indexOf(target.id);
|
||||
if (!nowExpanded) {
|
||||
const collapesArray: number[] = [];
|
||||
for (let i = expandItemIndex + 1; i < this.filterFlattenBranch.length; i++) {
|
||||
const node = root.getTreeNodeById(this.filterFlattenBranch[i]);
|
||||
if (node && node.depth > target.depth) {
|
||||
collapesArray.push(node.id);
|
||||
} else {
|
||||
break;
|
||||
this.filterWatcherDisposeCollection.push(
|
||||
root.watcher.on(TreeNodeEvent.DidChangeExpansionState, (target, nowExpanded) => {
|
||||
const expandItemIndex = this.filterFlattenBranch.indexOf(target.id);
|
||||
if (!nowExpanded) {
|
||||
const collapesArray: number[] = [];
|
||||
for (let i = expandItemIndex + 1; i < this.filterFlattenBranch.length; i++) {
|
||||
const node = root.getTreeNodeById(this.filterFlattenBranch[i]);
|
||||
if (node && node.depth > target.depth) {
|
||||
collapesArray.push(node.id);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.filterFlattenBranchChildrenCache.set(target.id, collapesArray);
|
||||
this.filterFlattenBranch = spliceArray(this.filterFlattenBranch, expandItemIndex + 1, collapesArray.length);
|
||||
} else {
|
||||
const spliceUint32Array = this.filterFlattenBranchChildrenCache.get(target.id);
|
||||
if (spliceUint32Array && spliceUint32Array.length > 0) {
|
||||
this.filterFlattenBranch = spliceArray(this.filterFlattenBranch, expandItemIndex + 1, 0, spliceUint32Array);
|
||||
this.filterFlattenBranchChildrenCache.delete(target.id);
|
||||
}
|
||||
}
|
||||
this.filterFlattenBranchChildrenCache.set(target.id, collapesArray);
|
||||
this.filterFlattenBranch = spliceArray(this.filterFlattenBranch, expandItemIndex + 1, collapesArray.length);
|
||||
} else {
|
||||
const spliceUint32Array = this.filterFlattenBranchChildrenCache.get(target.id);
|
||||
if (spliceUint32Array && spliceUint32Array.length > 0) {
|
||||
this.filterFlattenBranch = spliceArray(this.filterFlattenBranch, expandItemIndex + 1, 0, spliceUint32Array);
|
||||
this.filterFlattenBranchChildrenCache.delete(target.id);
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.filterWatcherDisposeCollection.push(Disposable.create(() => {
|
||||
this.filterFlattenBranchChildrenCache.clear();
|
||||
}));
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.filterWatcherDisposeCollection.push(
|
||||
Disposable.create(() => {
|
||||
this.filterFlattenBranchChildrenCache.clear();
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
private renderItem = ({ index, style }): JSX.Element => {
|
||||
this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
|
||||
|
@ -798,7 +841,7 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
return RecycleTree.DEFAULT_ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
let size: number = 0;
|
||||
let size = 0;
|
||||
if (wrapRef.current) {
|
||||
const ref = wrapRef.current as unknown as HTMLDivElement;
|
||||
size = Array.from(ref.children).reduce((pre, cur: HTMLElement) => pre + cur.getBoundingClientRect().height, 0);
|
||||
|
@ -810,20 +853,26 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
return Math.max(size, RecycleTree.DEFAULT_ITEM_HEIGHT);
|
||||
};
|
||||
|
||||
const itemStyle = overflow === 'ellipsis' ? style : { ...style, width: 'auto', minWidth: '100%', height: `${calcDynamicHeight()}px` };
|
||||
const itemStyle =
|
||||
overflow === 'ellipsis'
|
||||
? style
|
||||
: { ...style, width: 'auto', minWidth: '100%', height: `${calcDynamicHeight()}px` };
|
||||
|
||||
return <div ref={wrapRef} style={itemStyle} role={item.accessibilityInformation?.role || 'treeiem'} {...ariaInfo}>
|
||||
<NodeRendererWrap
|
||||
item={item}
|
||||
depth={item.depth}
|
||||
itemType={type}
|
||||
template={template}
|
||||
hasPrompt={!!this.promptHandle && !this.promptHandle.destroyed}
|
||||
expanded={CompositeTreeNode.is(item) ? (item as CompositeTreeNode).expanded : void 0}>
|
||||
{children as INodeRenderer}
|
||||
</NodeRendererWrap>
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div ref={wrapRef} style={itemStyle} role={item.accessibilityInformation?.role || 'treeiem'} {...ariaInfo}>
|
||||
<NodeRendererWrap
|
||||
item={item}
|
||||
depth={item.depth}
|
||||
itemType={type}
|
||||
template={template}
|
||||
hasPrompt={!!this.promptHandle && !this.promptHandle.destroyed}
|
||||
expanded={CompositeTreeNode.is(item) ? (item as CompositeTreeNode).expanded : void 0}
|
||||
>
|
||||
{children as INodeRenderer}
|
||||
</NodeRendererWrap>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private layoutItem = () => {
|
||||
if (!this.props.supportDynamicHeights) {
|
||||
|
@ -842,7 +891,7 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private getItemSize(index: number) {
|
||||
return this.dynamicSizeMap.get(index) || this.props.itemHeight;
|
||||
|
@ -866,7 +915,7 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
const Placeholder = placeholder;
|
||||
return <Placeholder />;
|
||||
}
|
||||
const addonProps: {[key in keyof ListProps]: any} = {
|
||||
const addonProps: { [key in keyof ListProps]: any } = {
|
||||
children,
|
||||
height,
|
||||
width,
|
||||
|
@ -886,13 +935,18 @@ export class RecycleTree extends React.Component<IRecycleTreeProps> {
|
|||
addonProps.innerElementType = InnerElementType;
|
||||
}
|
||||
|
||||
return (
|
||||
supportDynamicHeights
|
||||
? <VariableSizeList ref={this.listRef as React.RefObject<VariableSizeList>} itemSize={this.getItemSize.bind(this)} {...addonProps}>
|
||||
return supportDynamicHeights ? (
|
||||
<VariableSizeList
|
||||
ref={this.listRef as React.RefObject<VariableSizeList>}
|
||||
itemSize={this.getItemSize.bind(this)}
|
||||
{...addonProps}
|
||||
>
|
||||
{this.renderItem}
|
||||
</VariableSizeList>
|
||||
: <FixedSizeList ref={this.listRef as React.RefObject<FixedSizeList>} itemSize={itemHeight} {...addonProps}>
|
||||
) : (
|
||||
<FixedSizeList ref={this.listRef as React.RefObject<FixedSizeList>} itemSize={itemHeight} {...addonProps}>
|
||||
{this.renderItem}
|
||||
</FixedSizeList>);
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,11 @@ interface IRenamePromptRendererProps {
|
|||
itemType: TreeNodeType.RenamePrompt;
|
||||
}
|
||||
|
||||
export type INodeRendererProps = ITreeNodeRendererProps | ICompositeTreeNodeRendererProps | INewPromptRendererProps | IRenamePromptRendererProps;
|
||||
export type INodeRendererProps =
|
||||
| ITreeNodeRendererProps
|
||||
| ICompositeTreeNodeRendererProps
|
||||
| INewPromptRendererProps
|
||||
| IRenamePromptRendererProps;
|
||||
|
||||
export type INodeRenderer = (props: any) => JSX.Element;
|
||||
|
||||
|
@ -38,10 +42,9 @@ export interface INodeRendererWrapProps {
|
|||
}
|
||||
|
||||
export class NodeRendererWrap extends React.Component<INodeRendererWrapProps> {
|
||||
|
||||
public render() {
|
||||
const { item, itemType, children, template, hasPrompt } = this.props;
|
||||
return React.createElement(children, {item, itemType, template, hasPrompt, key: item.id});
|
||||
return React.createElement(children, { item, itemType, template, hasPrompt, key: item.id });
|
||||
}
|
||||
|
||||
public shouldComponentUpdate(nextProps: INodeRendererWrapProps) {
|
||||
|
|
|
@ -43,29 +43,32 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
|
|||
pageY: number;
|
||||
};
|
||||
activeNode?: BasicCompositeTreeNode | BasicTreeNode;
|
||||
}>({show: false});
|
||||
}>({ show: false });
|
||||
const [menubarItems, setMenubarItems] = useState<IBasicTreeMenu[]>([]);
|
||||
const [model, setModel] = useState<BasicTreeModel | undefined>();
|
||||
const treeService = useRef<BasicTreeService>(new BasicTreeService(treeData, resolveChildren, sortComparator));
|
||||
const treeHandle = useRef<IRecycleTreeHandle>();
|
||||
const wrapperRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
const renderTreeNode = useCallback((props: INodeRendererWrapProps) => {
|
||||
return <BasicTreeNodeRenderer
|
||||
item={props.item as any}
|
||||
itemType={props.itemType}
|
||||
itemHeight={itemHeight}
|
||||
indent={indent}
|
||||
className={itemClassname}
|
||||
inlineMenus={inlineMenus}
|
||||
inlineMenuActuator={inlineMenuActuator}
|
||||
onClick={handleItemClick}
|
||||
onDbClick={handleItemDbClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onTwistierClick={handleTwistierClick}
|
||||
decorations={treeService.current.decorations.getDecorations(props.item as ITreeNodeOrCompositeTreeNode)}
|
||||
/>;
|
||||
}, []);
|
||||
const renderTreeNode = useCallback(
|
||||
(props: INodeRendererWrapProps) => (
|
||||
<BasicTreeNodeRenderer
|
||||
item={props.item as any}
|
||||
itemType={props.itemType}
|
||||
itemHeight={itemHeight}
|
||||
indent={indent}
|
||||
className={itemClassname}
|
||||
inlineMenus={inlineMenus}
|
||||
inlineMenuActuator={inlineMenuActuator}
|
||||
onClick={handleItemClick}
|
||||
onDbClick={handleItemDbClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onTwistierClick={handleTwistierClick}
|
||||
decorations={treeService.current.decorations.getDecorations(props.item as ITreeNodeOrCompositeTreeNode)}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
ensureLoaded();
|
||||
|
@ -100,63 +103,70 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
|
|||
treeHandle.current = handle;
|
||||
}, []);
|
||||
|
||||
const handleItemClick = useCallback((event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
treeService.current?.activeFocusedDecoration(item);
|
||||
if (onClick) {
|
||||
onClick(event, item);
|
||||
}
|
||||
if (BasicCompositeTreeNode.is(item)) {
|
||||
toggleDirectory(item);
|
||||
}
|
||||
}, [onClick]);
|
||||
|
||||
const handleItemDbClick = useCallback((event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (onDbClick) {
|
||||
onDbClick(event, item);
|
||||
}
|
||||
}, [onDbClick]);
|
||||
|
||||
const handleContextMenu = useCallback((event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (item) {
|
||||
treeService.current?.activeContextMenuDecoration(item);
|
||||
} else {
|
||||
treeService.current?.enactiveFocusedDecoration();
|
||||
}
|
||||
if (onContextMenu) {
|
||||
onContextMenu(event, item);
|
||||
} else {
|
||||
// let menus: IBasicTreeMenu[] = [];
|
||||
let rawMenus: IBasicContextMenu[] = [];
|
||||
if (Array.isArray(contextMenus)) {
|
||||
rawMenus = contextMenus;
|
||||
} else if (typeof contextMenus === 'function') {
|
||||
rawMenus = contextMenus(item);
|
||||
const handleItemClick = useCallback(
|
||||
(event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
treeService.current?.activeFocusedDecoration(item);
|
||||
if (onClick) {
|
||||
onClick(event, item);
|
||||
}
|
||||
const groups = new Set<string>();
|
||||
const menusMap = {};
|
||||
for (const menu of rawMenus) {
|
||||
groups.add(menu.group || '-1');
|
||||
if (!menusMap[menu.group || '-1']) {
|
||||
menusMap[menu.group || '-1'] = [];
|
||||
if (BasicCompositeTreeNode.is(item)) {
|
||||
toggleDirectory(item);
|
||||
}
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
const handleItemDbClick = useCallback(
|
||||
(event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (onDbClick) {
|
||||
onDbClick(event, item);
|
||||
}
|
||||
},
|
||||
[onDbClick],
|
||||
);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (item) {
|
||||
treeService.current?.activeContextMenuDecoration(item);
|
||||
} else {
|
||||
treeService.current?.enactiveFocusedDecoration();
|
||||
}
|
||||
if (onContextMenu) {
|
||||
onContextMenu(event, item);
|
||||
} else {
|
||||
// let menus: IBasicTreeMenu[] = [];
|
||||
let rawMenus: IBasicContextMenu[] = [];
|
||||
if (Array.isArray(contextMenus)) {
|
||||
rawMenus = contextMenus;
|
||||
} else if (typeof contextMenus === 'function') {
|
||||
rawMenus = contextMenus(item);
|
||||
}
|
||||
menusMap[menu.group || '-1'].push(menu);
|
||||
const groups = new Set<string>();
|
||||
const menusMap = {};
|
||||
for (const menu of rawMenus) {
|
||||
groups.add(menu.group || '-1');
|
||||
if (!menusMap[menu.group || '-1']) {
|
||||
menusMap[menu.group || '-1'] = [];
|
||||
}
|
||||
menusMap[menu.group || '-1'].push(menu);
|
||||
}
|
||||
const sortGroup = Array.from(groups).sort((a, b) => a.localeCompare(b, 'kn', { numeric: true }));
|
||||
let menus: IBasicTreeMenu[] = [];
|
||||
for (const group of sortGroup) {
|
||||
menus = menus.concat(menusMap[group].map((menu) => ({ id: menu.id, label: menu.title, group: menu.group })));
|
||||
menus = menus.concat([{ id: `${group}_divider`, label: '', type: 'divider' }]);
|
||||
}
|
||||
menus.pop();
|
||||
if (JSON.stringify(menus) !== JSON.stringify(menubarItems)) {
|
||||
setMenubarItems(menus);
|
||||
}
|
||||
const { x, y } = event.nativeEvent;
|
||||
setShowMenus({ show: true, point: { pageX: x, pageY: y }, activeNode: item });
|
||||
}
|
||||
const sortGroup = Array.from(groups).sort((a, b) => {
|
||||
return a.localeCompare(b, 'kn', { numeric: true });
|
||||
});
|
||||
let menus: IBasicTreeMenu[] = [];
|
||||
for (const group of sortGroup) {
|
||||
menus = menus.concat(menusMap[group].map((menu) => ({ id: menu.id, label: menu.title, group: menu.group })));
|
||||
menus = menus.concat([{ id: `${group}_divider`, label: '', type: 'divider' }]);
|
||||
}
|
||||
menus.pop();
|
||||
if (JSON.stringify(menus) !== JSON.stringify(menubarItems)) {
|
||||
setMenubarItems(menus);
|
||||
}
|
||||
const { x, y } = event.nativeEvent;
|
||||
setShowMenus({ show: true, point: { pageX: x, pageY: y }, activeNode: item});
|
||||
}
|
||||
}, [onDbClick]);
|
||||
},
|
||||
[onDbClick],
|
||||
);
|
||||
|
||||
const toggleDirectory = useCallback((item: BasicCompositeTreeNode) => {
|
||||
if (item.expanded) {
|
||||
|
@ -166,24 +176,30 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const handleTwistierClick = useCallback((event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (BasicCompositeTreeNode.is(item)) {
|
||||
toggleDirectory(item);
|
||||
}
|
||||
if (onTwistierClick) {
|
||||
onTwistierClick(event, item);
|
||||
}
|
||||
}, [onTwistierClick]);
|
||||
const handleTwistierClick = useCallback(
|
||||
(event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (BasicCompositeTreeNode.is(item)) {
|
||||
toggleDirectory(item);
|
||||
}
|
||||
if (onTwistierClick) {
|
||||
onTwistierClick(event, item);
|
||||
}
|
||||
},
|
||||
[onTwistierClick],
|
||||
);
|
||||
|
||||
const handleOuterClick = useCallback(() => {
|
||||
treeService.current?.enactiveFocusedDecoration();
|
||||
}, []);
|
||||
|
||||
const handleOuterContextMenu = useCallback((event: React.MouseEvent, item?: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (onContextMenu) {
|
||||
onContextMenu(event);
|
||||
}
|
||||
}, []);
|
||||
const handleOuterContextMenu = useCallback(
|
||||
(event: React.MouseEvent, item?: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (onContextMenu) {
|
||||
onContextMenu(event);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setShowMenus({ ...showMenus, show: false });
|
||||
|
@ -193,67 +209,69 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
|
|||
if (!contextMenus) {
|
||||
return null;
|
||||
}
|
||||
return <CtxMenuTrigger
|
||||
popupPlacement='bottomLeft'
|
||||
popupVisible={showMenus.show}
|
||||
action={['contextMenu']}
|
||||
popupAlign={{
|
||||
overflow: {
|
||||
adjustX: 1,
|
||||
adjustY: 1,
|
||||
},
|
||||
offset: [window.scrollX, window.scrollY],
|
||||
}}
|
||||
point={showMenus.point || {}}
|
||||
builtinPlacements={placements}
|
||||
popup={(
|
||||
<ClickOutside
|
||||
className='basic_tree_menubars'
|
||||
mouseEvents={['click', 'contextmenu']}
|
||||
onOutsideClick={handleMouseLeave}>
|
||||
{
|
||||
menubarItems.map(({ id, label, type }) => (
|
||||
<BasicMenuItem
|
||||
key={id}
|
||||
id={id}
|
||||
label={label}
|
||||
type={type}
|
||||
focusMode={showMenus.show}
|
||||
onClick={(id: string) => {
|
||||
if (contextMenuActuator) {
|
||||
contextMenuActuator(showMenus.activeNode!, id);
|
||||
}
|
||||
setShowMenus({ show: false });
|
||||
}} />
|
||||
))
|
||||
return (
|
||||
<CtxMenuTrigger
|
||||
popupPlacement='bottomLeft'
|
||||
popupVisible={showMenus.show}
|
||||
action={['contextMenu']}
|
||||
popupAlign={{
|
||||
overflow: {
|
||||
adjustX: 1,
|
||||
adjustY: 1,
|
||||
},
|
||||
offset: [window.scrollX, window.scrollY],
|
||||
}}
|
||||
point={showMenus.point || {}}
|
||||
builtinPlacements={placements}
|
||||
popup={
|
||||
<ClickOutside
|
||||
className='basic_tree_menubars'
|
||||
mouseEvents={['click', 'contextmenu']}
|
||||
onOutsideClick={handleMouseLeave}
|
||||
>
|
||||
{menubarItems.map(({ id, label, type }) => (
|
||||
<BasicMenuItem
|
||||
key={id}
|
||||
id={id}
|
||||
label={label}
|
||||
type={type}
|
||||
focusMode={showMenus.show}
|
||||
onClick={(id: string) => {
|
||||
if (contextMenuActuator) {
|
||||
contextMenuActuator(showMenus.activeNode!, id);
|
||||
}
|
||||
setShowMenus({ show: false });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ClickOutside>
|
||||
}
|
||||
</ClickOutside>
|
||||
)}
|
||||
alignPoint
|
||||
/>;
|
||||
alignPoint
|
||||
/>
|
||||
);
|
||||
}, [menubarItems, contextMenuActuator, showMenus]);
|
||||
|
||||
return <div
|
||||
className='basic_tree'
|
||||
tabIndex={-1}
|
||||
ref={wrapperRef}
|
||||
onClick={handleOuterClick}
|
||||
onContextMenu={handleOuterContextMenu}
|
||||
>
|
||||
{ renderContextMenu() }
|
||||
{
|
||||
model
|
||||
? <RecycleTree
|
||||
height={height}
|
||||
width={width}
|
||||
itemHeight={itemHeight}
|
||||
model={model}
|
||||
onReady={handleTreeReady}
|
||||
className={cls(containerClassname)}
|
||||
>
|
||||
{renderTreeNode}
|
||||
</RecycleTree>
|
||||
: null
|
||||
}
|
||||
</div>;
|
||||
return (
|
||||
<div
|
||||
className='basic_tree'
|
||||
tabIndex={-1}
|
||||
ref={wrapperRef}
|
||||
onClick={handleOuterClick}
|
||||
onContextMenu={handleOuterContextMenu}
|
||||
>
|
||||
{renderContextMenu()}
|
||||
{model ? (
|
||||
<RecycleTree
|
||||
height={height}
|
||||
width={width}
|
||||
itemHeight={itemHeight}
|
||||
model={model}
|
||||
onReady={handleTreeReady}
|
||||
className={cls(containerClassname)}
|
||||
>
|
||||
{renderTreeNode}
|
||||
</RecycleTree>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,10 +2,12 @@ import React from 'react';
|
|||
import clx from 'classnames';
|
||||
import { IBasicTreeMenu } from './types';
|
||||
|
||||
export const BasicMenuItem: React.FC<IBasicTreeMenu & {
|
||||
focusMode: boolean;
|
||||
onClick: (id: string) => void;
|
||||
}> = ({ id, label, type, focusMode, onClick }) => {
|
||||
export const BasicMenuItem: React.FC<
|
||||
IBasicTreeMenu & {
|
||||
focusMode: boolean;
|
||||
onClick: (id: string) => void;
|
||||
}
|
||||
> = ({ id, label, type, focusMode, onClick }) => {
|
||||
const [menuOpen, setMenuOpen] = React.useState<boolean>(false);
|
||||
|
||||
const handleMenuItemClick = React.useCallback(() => {
|
||||
|
@ -17,8 +19,7 @@ export const BasicMenuItem: React.FC<IBasicTreeMenu & {
|
|||
onClick(id);
|
||||
}, [id]);
|
||||
|
||||
const handleMouseOver = React.useCallback(() => {
|
||||
}, [id, focusMode]);
|
||||
const handleMouseOver = React.useCallback(() => {}, [id, focusMode]);
|
||||
|
||||
if (type === 'divider') {
|
||||
return <div className='basic_menu_item_divider'></div>;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
flex-direction: column;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
&:hover {
|
||||
color: var(--kt-tree-hoverForeground);
|
||||
background: var(--kt-tree-hoverBackground);
|
||||
|
@ -23,14 +23,14 @@
|
|||
color: var(--kt-tree-inactiveSelectionForeground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.mod_focused {
|
||||
outline: 1px solid var(--list-focusOutline);
|
||||
outline-offset: -1px;
|
||||
color: var(--kt-tree-activeSelectionForeground) !important;;
|
||||
color: var(--kt-tree-activeSelectionForeground) !important;
|
||||
background: var(--kt-tree-activeSelectionBackground);
|
||||
.expansion_toggle {
|
||||
color: var(--kt-tree-activeSelectionForeground) !important;;
|
||||
color: var(--kt-tree-activeSelectionForeground) !important;
|
||||
}
|
||||
.displayname {
|
||||
.compact_name {
|
||||
|
@ -40,7 +40,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.mod_actived {
|
||||
outline: 1px solid var(--list-focusOutline);
|
||||
outline-offset: -1px;
|
||||
|
@ -55,9 +55,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.mod_loading {
|
||||
opacity: .8 !important;;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@
|
|||
}
|
||||
|
||||
.status {
|
||||
opacity: .75;
|
||||
opacity: 0.75;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding-right: 0;
|
||||
|
@ -116,7 +116,7 @@
|
|||
.displayname {
|
||||
margin-right: 6px;
|
||||
display: inline;
|
||||
white-space:pre;
|
||||
white-space: pre;
|
||||
.compact_name {
|
||||
color: inherit;
|
||||
display: inline-block;
|
||||
|
@ -130,14 +130,14 @@
|
|||
}
|
||||
.compact_name_separator {
|
||||
margin: 0 2px;
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.display_name {
|
||||
margin-right: 6px;
|
||||
display: inline;
|
||||
white-space:pre;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
@ -242,4 +242,4 @@
|
|||
color: var(--menu-selectionForeground);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,7 @@ import { IBasicTreeData } from './types';
|
|||
|
||||
export class BasicTreeRoot extends CompositeTreeNode {
|
||||
private _raw: IBasicTreeData;
|
||||
constructor(
|
||||
tree: ITree,
|
||||
parent: BasicCompositeTreeNode | undefined,
|
||||
data: IBasicTreeData,
|
||||
) {
|
||||
constructor(tree: ITree, parent: BasicCompositeTreeNode | undefined, data: IBasicTreeData) {
|
||||
super(tree, parent);
|
||||
this._raw = data;
|
||||
}
|
||||
|
@ -31,12 +27,7 @@ export class BasicCompositeTreeNode extends CompositeTreeNode {
|
|||
private _whenReady: Promise<void>;
|
||||
private _raw: IBasicTreeData;
|
||||
|
||||
constructor(
|
||||
tree: ITree,
|
||||
parent: BasicCompositeTreeNode | undefined,
|
||||
data: IBasicTreeData,
|
||||
id?: number,
|
||||
) {
|
||||
constructor(tree: ITree, parent: BasicCompositeTreeNode | undefined, data: IBasicTreeData, id?: number) {
|
||||
super(tree, parent, undefined, {}, { disableCache: true });
|
||||
if (data.expanded) {
|
||||
this._whenReady = this.setExpanded();
|
||||
|
@ -74,12 +65,7 @@ export class BasicTreeNode extends TreeNode {
|
|||
private _displayName: string;
|
||||
private _raw: IBasicTreeData;
|
||||
|
||||
constructor(
|
||||
tree: ITree,
|
||||
parent: BasicCompositeTreeNode | undefined,
|
||||
data: IBasicTreeData,
|
||||
id?: number,
|
||||
) {
|
||||
constructor(tree: ITree, parent: BasicCompositeTreeNode | undefined, data: IBasicTreeData, id?: number) {
|
||||
super(tree, parent, undefined, {}, { disableCache: true });
|
||||
this._uid = id || this._uid;
|
||||
// 每个节点应该拥有自己独立的路径,不存在重复性
|
||||
|
|
|
@ -7,7 +7,9 @@ import { BasicCompositeTreeNode, BasicTreeNode } from './tree-node.define';
|
|||
import cls from 'classnames';
|
||||
import './styles.less';
|
||||
|
||||
export const BasicTreeNodeRenderer: React.FC<IBasicNodeRendererProps & { item: BasicCompositeTreeNode | BasicTreeNode }> = ({
|
||||
export const BasicTreeNodeRenderer: React.FC<
|
||||
IBasicNodeRendererProps & { item: BasicCompositeTreeNode | BasicTreeNode }
|
||||
> = ({
|
||||
item,
|
||||
className,
|
||||
itemHeight = 22,
|
||||
|
@ -20,37 +22,49 @@ export const BasicTreeNodeRenderer: React.FC<IBasicNodeRendererProps & { item: B
|
|||
inlineMenus = [],
|
||||
inlineMenuActuator = () => {},
|
||||
}: IBasicNodeRendererProps & { item: BasicCompositeTreeNode | BasicTreeNode }) => {
|
||||
const handleClick = useCallback((event: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
event.stopPropagation();
|
||||
onClick(event, item as any);
|
||||
}
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
const handleDbClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (onDbClick) {
|
||||
event.stopPropagation();
|
||||
onDbClick(event, item as any);
|
||||
}
|
||||
},
|
||||
[onDbClick],
|
||||
);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (onContextMenu) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onContextMenu(event, item as any);
|
||||
}
|
||||
},
|
||||
[onContextMenu],
|
||||
);
|
||||
|
||||
const handlerTwistierClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onClick(event, item as any);
|
||||
}
|
||||
}, [onClick]);
|
||||
|
||||
const handleDbClick = useCallback((event: React.MouseEvent) => {
|
||||
if (onDbClick) {
|
||||
event.stopPropagation();
|
||||
onDbClick(event, item as any);
|
||||
}
|
||||
}, [onDbClick]);
|
||||
|
||||
const handleContextMenu = useCallback((event: React.MouseEvent) => {
|
||||
if (onContextMenu) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onContextMenu(event, item as any);
|
||||
}
|
||||
}, [onContextMenu]);
|
||||
|
||||
const handlerTwistierClick = useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (onTwistierClick) {
|
||||
onTwistierClick(event, item as any);
|
||||
} else if (onClick) {
|
||||
onClick(event, item as any);
|
||||
}
|
||||
}, [onClick, onTwistierClick]);
|
||||
if (onTwistierClick) {
|
||||
onTwistierClick(event, item as any);
|
||||
} else if (onClick) {
|
||||
onClick(event, item as any);
|
||||
}
|
||||
},
|
||||
[onClick, onTwistierClick],
|
||||
);
|
||||
|
||||
const paddingLeft = `${8 + (item.depth || 0) * (indent || 0) + (!BasicCompositeTreeNode.is(item) ? 20 : 0)}px`;
|
||||
|
||||
|
@ -60,44 +74,50 @@ export const BasicTreeNodeRenderer: React.FC<IBasicNodeRendererProps & { item: B
|
|||
paddingLeft,
|
||||
} as React.CSSProperties;
|
||||
|
||||
const renderIcon = useCallback((node: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
return <Icon icon={node.icon} className='icon' style={{ height: itemHeight, lineHeight: `${itemHeight}px`}}/>;
|
||||
}, []);
|
||||
const renderIcon = useCallback(
|
||||
(node: BasicCompositeTreeNode | BasicTreeNode) => (
|
||||
<Icon icon={node.icon} className='icon' style={{ height: itemHeight, lineHeight: `${itemHeight}px` }} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const getName = useCallback((node: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
return node.displayName.replace(/\n/g, '↵');
|
||||
}, []);
|
||||
const getName = useCallback(
|
||||
(node: BasicCompositeTreeNode | BasicTreeNode) => node.displayName.replace(/\n/g, '↵'),
|
||||
[],
|
||||
);
|
||||
|
||||
const renderDisplayName = useCallback((node: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
return (
|
||||
<div
|
||||
className={cls('segment', 'display_name')}
|
||||
>
|
||||
{getName(node)}
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
const renderDisplayName = useCallback(
|
||||
(node: BasicCompositeTreeNode | BasicTreeNode) => (
|
||||
<div className={cls('segment', 'display_name')}>{getName(node)}</div>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const renderDescription = useCallback((node: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (!node.description) {
|
||||
return null;
|
||||
}
|
||||
return <div className={cls('segment_grow', 'description')}>
|
||||
{node.description}
|
||||
</div>;
|
||||
return <div className={cls('segment_grow', 'description')}>{node.description}</div>;
|
||||
}, []);
|
||||
|
||||
const inlineMenuActions = useCallback((item: BasicCompositeTreeNode | BasicTreeNode ) => {
|
||||
if (Array.isArray(inlineMenus)) {
|
||||
return inlineMenus;
|
||||
} else if (typeof inlineMenus === 'function') {
|
||||
return inlineMenus(item);
|
||||
}
|
||||
}, [inlineMenus]);
|
||||
const inlineMenuActions = useCallback(
|
||||
(item: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (Array.isArray(inlineMenus)) {
|
||||
return inlineMenus;
|
||||
} else if (typeof inlineMenus === 'function') {
|
||||
return inlineMenus(item);
|
||||
}
|
||||
},
|
||||
[inlineMenus],
|
||||
);
|
||||
|
||||
const renderNodeTail = () => {
|
||||
const isBasicCompositeTreeNode = BasicCompositeTreeNode.is(item);
|
||||
const actions = inlineMenuActions(item)?.filter((menu) => isBasicCompositeTreeNode ? menu.position === IBasicInlineMenuPosition.TREE_CONTAINER : menu.position === IBasicInlineMenuPosition.TREE_NODE);
|
||||
const actions = inlineMenuActions(item)?.filter((menu) =>
|
||||
isBasicCompositeTreeNode
|
||||
? menu.position === IBasicInlineMenuPosition.TREE_CONTAINER
|
||||
: menu.position === IBasicInlineMenuPosition.TREE_NODE,
|
||||
);
|
||||
if (!actions?.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -105,35 +125,35 @@ export const BasicTreeNodeRenderer: React.FC<IBasicNodeRendererProps & { item: B
|
|||
event.stopPropagation();
|
||||
inlineMenuActuator(item, action);
|
||||
}, []);
|
||||
return <div className={cls('segment', 'tail')}>
|
||||
{
|
||||
actions.map((action) => {
|
||||
return <Button
|
||||
style={{marginRight: '5px'}}
|
||||
return (
|
||||
<div className={cls('segment', 'tail')}>
|
||||
{actions.map((action) => (
|
||||
<Button
|
||||
style={{ marginRight: '5px' }}
|
||||
type='icon'
|
||||
key={`${item.id}-${action.icon}`}
|
||||
icon={action.icon}
|
||||
title={action.title}
|
||||
onClick={(e) => handleActionClick(e, action)}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFolderToggle = (node: BasicCompositeTreeNode, clickHandler: any) => {
|
||||
if (decorations && decorations?.classlist.indexOf(DECORATIONS.LOADING) > -1) {
|
||||
return <Loading />;
|
||||
}
|
||||
return <Icon
|
||||
className={cls(
|
||||
'segment',
|
||||
'expansion_toggle',
|
||||
{ ['mod_collapsed']: !(node as BasicCompositeTreeNode).expanded },
|
||||
)}
|
||||
onClick={clickHandler}
|
||||
icon='arrow-right'
|
||||
/>;
|
||||
return (
|
||||
<Icon
|
||||
className={cls('segment', 'expansion_toggle', {
|
||||
['mod_collapsed']: !(node as BasicCompositeTreeNode).expanded,
|
||||
})}
|
||||
onClick={clickHandler}
|
||||
icon='arrow-right'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTwice = (item) => {
|
||||
|
@ -148,20 +168,14 @@ export const BasicTreeNodeRenderer: React.FC<IBasicNodeRendererProps & { item: B
|
|||
onClick={handleClick}
|
||||
onDoubleClick={handleDbClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={cls(
|
||||
'tree_node',
|
||||
className,
|
||||
decorations ? decorations.classlist : null,
|
||||
)}
|
||||
className={cls('tree_node', className, decorations ? decorations.classlist : null)}
|
||||
style={editorNodeStyle}
|
||||
data-id={item.id}
|
||||
>
|
||||
<div className='content'>
|
||||
{renderTwice(item)}
|
||||
{renderIcon(item)}
|
||||
<div
|
||||
className={'overflow_wrap'}
|
||||
>
|
||||
<div className={'overflow_wrap'}>
|
||||
{renderDisplayName(item)}
|
||||
{renderDescription(item)}
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@ export class BasicTreeService extends Tree {
|
|||
private _sortComparator?: (a: IBasicTreeData, b: IBasicTreeData) => number,
|
||||
) {
|
||||
super();
|
||||
this._root = new BasicTreeRoot(this, undefined, { children: this._treeData, label: '', command: '', icon: ''});
|
||||
this._root = new BasicTreeRoot(this, undefined, { children: this._treeData, label: '', command: '', icon: '' });
|
||||
this._model = new BasicTreeModel();
|
||||
this._model.init(this._root);
|
||||
this.initDecorations(this._root as BasicTreeRoot);
|
||||
|
@ -59,12 +59,16 @@ export class BasicTreeService extends Tree {
|
|||
this._decorations.addDecoration(this.focusedDecoration);
|
||||
this._decorations.addDecoration(this.contextMenuDecoration);
|
||||
this._decorations.addDecoration(this.loadingDecoration);
|
||||
this.disposableCollection.push(root.watcher.on(TreeNodeEvent.WillResolveChildren, (target) => {
|
||||
this.loadingDecoration.addTarget(target);
|
||||
}));
|
||||
this.disposableCollection.push(root.watcher.on(TreeNodeEvent.DidResolveChildren, (target) => {
|
||||
this.loadingDecoration.removeTarget(target);
|
||||
}));
|
||||
this.disposableCollection.push(
|
||||
root.watcher.on(TreeNodeEvent.WillResolveChildren, (target) => {
|
||||
this.loadingDecoration.addTarget(target);
|
||||
}),
|
||||
);
|
||||
this.disposableCollection.push(
|
||||
root.watcher.on(TreeNodeEvent.DidResolveChildren, (target) => {
|
||||
this.loadingDecoration.removeTarget(target);
|
||||
}),
|
||||
);
|
||||
this.disposableCollection.push(this._decorations);
|
||||
}
|
||||
|
||||
|
@ -81,7 +85,7 @@ export class BasicTreeService extends Tree {
|
|||
return this._sortComparator(a.raw, b.raw);
|
||||
}
|
||||
return super.sortComparator(a, b);
|
||||
}
|
||||
};
|
||||
|
||||
private transformTreeNode(parent?: BasicCompositeTreeNode, nodes?: IBasicTreeData[]) {
|
||||
if (!nodes) {
|
||||
|
@ -138,7 +142,7 @@ export class BasicTreeService extends Tree {
|
|||
|
||||
this.model?.dispatchChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
activeContextMenuDecoration = (target: BasicCompositeTreeNode | BasicTreeNode) => {
|
||||
if (this._contextMenuNode) {
|
||||
|
@ -151,7 +155,7 @@ export class BasicTreeService extends Tree {
|
|||
this.contextMenuDecoration.addTarget(target);
|
||||
this._contextMenuNode = target;
|
||||
this.model?.dispatchChange();
|
||||
}
|
||||
};
|
||||
|
||||
// 取消选中节点焦点
|
||||
enactiveFocusedDecoration = () => {
|
||||
|
@ -160,7 +164,7 @@ export class BasicTreeService extends Tree {
|
|||
this._focusedNode = undefined;
|
||||
this.model?.dispatchChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dispose() {
|
||||
this.disposableCollection.dispose();
|
||||
|
|
|
@ -130,8 +130,8 @@ export interface IBasicRecycleTreeProps {
|
|||
*/
|
||||
onContextMenu?: (event: React.MouseEvent, node?: ITreeNodeOrCompositeTreeNode) => void;
|
||||
/**
|
||||
* 箭头点击事件
|
||||
*/
|
||||
* 箭头点击事件
|
||||
*/
|
||||
onTwistierClick?: (event: React.MouseEvent, node: ITreeNodeOrCompositeTreeNode) => void;
|
||||
/**
|
||||
* 右键菜单定义,但传入了 `onContextMenu` 函数时将有限执行 `onContextMenu` 函数
|
||||
|
|
|
@ -5,111 +5,102 @@ import { TreeModel } from '../../tree';
|
|||
|
||||
type AdaptiveTreeHoc<Props, ExtraProps = any> = (
|
||||
Component: React.ComponentType<Props>,
|
||||
) => React.ComponentType<
|
||||
Props & (ExtraProps extends undefined ? never : ExtraProps)
|
||||
>;
|
||||
) => React.ComponentType<Props & (ExtraProps extends undefined ? never : ExtraProps)>;
|
||||
|
||||
/**
|
||||
* 将 RecycleTree 组件装饰到能够自适应高度
|
||||
* @param recycleTreeComp RecycleTree 组件
|
||||
*/
|
||||
export const RecycleTreeAdaptiveDecorator: AdaptiveTreeHoc<
|
||||
IRecycleTreeProps
|
||||
> = (recycleTreeComp) => (props: IRecycleTreeProps) => {
|
||||
const { model, itemHeight, onReady } = props;
|
||||
const ref = React.useRef<TreeModel>();
|
||||
const destroyWhileBlur = React.useRef<boolean>(false);
|
||||
export const RecycleTreeAdaptiveDecorator: AdaptiveTreeHoc<IRecycleTreeProps> =
|
||||
(recycleTreeComp) => (props: IRecycleTreeProps) => {
|
||||
const { model, itemHeight, onReady } = props;
|
||||
const ref = React.useRef<TreeModel>();
|
||||
const destroyWhileBlur = React.useRef<boolean>(false);
|
||||
|
||||
const [currentHeight, setCurrentHeight] = React.useState<number>(0);
|
||||
const [currentHeight, setCurrentHeight] = React.useState<number>(0);
|
||||
|
||||
const handleExpansionChange = () => {
|
||||
setCurrentHeight(model.root.branchSize * itemHeight);
|
||||
};
|
||||
const handleExpansionChange = () => {
|
||||
setCurrentHeight(model.root.branchSize * itemHeight);
|
||||
};
|
||||
|
||||
const handleOnReady = (handle: IRecycleTreeHandle) => {
|
||||
if (!onReady) {
|
||||
return;
|
||||
}
|
||||
onReady({
|
||||
...handle,
|
||||
promptNewTreeNode: async (at) => {
|
||||
const promptHandle = await handle.promptNewTreeNode(at);
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight + itemHeight);
|
||||
} else {
|
||||
// 添加节点时,如果不存在 ref.current,即不存在可渲染节点
|
||||
// 补全高度便于插入输入框
|
||||
setCurrentHeight(itemHeight);
|
||||
}
|
||||
destroyWhileBlur.current = false;
|
||||
promptHandle.onDestroy(() => {
|
||||
const handleOnReady = (handle: IRecycleTreeHandle) => {
|
||||
if (!onReady) {
|
||||
return;
|
||||
}
|
||||
onReady({
|
||||
...handle,
|
||||
promptNewTreeNode: async (at) => {
|
||||
const promptHandle = await handle.promptNewTreeNode(at);
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight + itemHeight);
|
||||
} else {
|
||||
// 添加节点时,如果不存在 ref.current,即不存在可渲染节点
|
||||
// 补全高度便于插入输入框
|
||||
setCurrentHeight(itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onCancel(() => {
|
||||
if (ref.current) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onBlur(() => {
|
||||
return destroyWhileBlur.current;
|
||||
});
|
||||
return promptHandle;
|
||||
},
|
||||
promptNewCompositeTreeNode: async (at) => {
|
||||
const promptHandle = await handle.promptNewCompositeTreeNode(at);
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight + itemHeight);
|
||||
} else {
|
||||
// 添加节点时,如果不存在 ref.current,即不存在可渲染节点
|
||||
// 补全高度便于插入输入框
|
||||
setCurrentHeight(itemHeight);
|
||||
}
|
||||
promptHandle.onDestroy(() => {
|
||||
destroyWhileBlur.current = false;
|
||||
promptHandle.onDestroy(() => {
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onCancel(() => {
|
||||
if (ref.current) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onBlur(() => destroyWhileBlur.current);
|
||||
return promptHandle;
|
||||
},
|
||||
promptNewCompositeTreeNode: async (at) => {
|
||||
const promptHandle = await handle.promptNewCompositeTreeNode(at);
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight + itemHeight);
|
||||
} else {
|
||||
// 添加节点时,如果不存在 ref.current,即不存在可渲染节点
|
||||
// 补全高度便于插入输入框
|
||||
setCurrentHeight(itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onCancel(() => {
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onBlur(() => {
|
||||
return destroyWhileBlur.current;
|
||||
});
|
||||
return promptHandle;
|
||||
},
|
||||
});
|
||||
};
|
||||
promptHandle.onDestroy(() => {
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onCancel(() => {
|
||||
if (ref.current?.root.branchSize) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
}
|
||||
});
|
||||
promptHandle.onBlur(() => destroyWhileBlur.current);
|
||||
return promptHandle;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
ref.current = model;
|
||||
if (ref.current.root) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
ref.current.root.watcher.on(TreeNodeEvent.DidChangeExpansionState, handleExpansionChange);
|
||||
}
|
||||
}, [model]);
|
||||
React.useEffect(() => {
|
||||
ref.current = model;
|
||||
if (ref.current.root) {
|
||||
setCurrentHeight(ref.current.root.branchSize * itemHeight);
|
||||
ref.current.root.watcher.on(TreeNodeEvent.DidChangeExpansionState, handleExpansionChange);
|
||||
}
|
||||
}, [model]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// currentHeight 更新时,有概率让整个 Tree 重绘
|
||||
// 导致 prompt 的 blur 事件触发
|
||||
// 这里等待 100 ms 后再将 prompt 失去焦点后关闭的逻辑打开
|
||||
setTimeout(() => {
|
||||
destroyWhileBlur.current = true;
|
||||
}, 100);
|
||||
}, [currentHeight]);
|
||||
React.useEffect(() => {
|
||||
// currentHeight 更新时,有概率让整个 Tree 重绘
|
||||
// 导致 prompt 的 blur 事件触发
|
||||
// 这里等待 100 ms 后再将 prompt 失去焦点后关闭的逻辑打开
|
||||
setTimeout(() => {
|
||||
destroyWhileBlur.current = true;
|
||||
}, 100);
|
||||
}, [currentHeight]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
React.createElement(recycleTreeComp, {
|
||||
return (
|
||||
<>
|
||||
{React.createElement(recycleTreeComp, {
|
||||
...props,
|
||||
height: currentHeight,
|
||||
onReady: handleOnReady,
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,9 +13,7 @@ const TREE_FILTER_DELAY = 500;
|
|||
|
||||
type FilterHoc<Props, ExtraProps = any> = (
|
||||
Component: React.ComponentType<Props>,
|
||||
) => React.ComponentType<
|
||||
Props & (ExtraProps extends undefined ? never : ExtraProps)
|
||||
>;
|
||||
) => React.ComponentType<Props & (ExtraProps extends undefined ? never : ExtraProps)>;
|
||||
|
||||
const FilterInput: React.FC<IInputBaseProps> = (props) => {
|
||||
const { localize } = React.useContext(LocalizeContext);
|
||||
|
@ -29,12 +27,13 @@ const FilterInput: React.FC<IInputBaseProps> = (props) => {
|
|||
size='small'
|
||||
{...props}
|
||||
placeholder={props.placeholder || localize('tree.filter.placeholder')}
|
||||
addonBefore={<Icon className='kt-recycle-tree-filter-icon' icon='retrieval' />} />
|
||||
addonBefore={<Icon className='kt-recycle-tree-filter-icon' icon='retrieval' />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IRecycleTreeFilterHandle extends IRecycleTreeHandle {
|
||||
export interface IRecycleTreeFilterHandle extends IRecycleTreeHandle {
|
||||
clearFilter: () => void;
|
||||
}
|
||||
|
||||
|
@ -50,12 +49,12 @@ export interface IRecycleTreeFilterHandle extends IRecycleTreeHandle {
|
|||
export const RecycleTreeFilterDecorator: FilterHoc<
|
||||
IRecycleTreeProps,
|
||||
{
|
||||
filterEnabled?: boolean,
|
||||
filterEnabled?: boolean;
|
||||
// 用于在filter变化前进行额外处理,例如展开所有目录
|
||||
beforeFilterValueChange?: (value: string) => Promise<void>;
|
||||
filterAfterClear?: IInputBaseProps['afterClear'],
|
||||
filterPlaceholder?: IInputBaseProps['placeholder'],
|
||||
filterAutoFocus?: IInputBaseProps['autoFocus'],
|
||||
filterAfterClear?: IInputBaseProps['afterClear'];
|
||||
filterPlaceholder?: IInputBaseProps['placeholder'];
|
||||
filterAutoFocus?: IInputBaseProps['autoFocus'];
|
||||
}
|
||||
> = (recycleTreeComp) => (props) => {
|
||||
const [value, setValue] = React.useState<string>('');
|
||||
|
@ -63,9 +62,14 @@ export const RecycleTreeFilterDecorator: FilterHoc<
|
|||
const [filter, setFilter] = React.useState<string>('');
|
||||
|
||||
const {
|
||||
beforeFilterValueChange, filterEnabled, height,
|
||||
filterPlaceholder, filterAfterClear, onReady,
|
||||
filterAutoFocus, ...recycleTreeProps
|
||||
beforeFilterValueChange,
|
||||
filterEnabled,
|
||||
height,
|
||||
filterPlaceholder,
|
||||
filterAfterClear,
|
||||
onReady,
|
||||
filterAutoFocus,
|
||||
...recycleTreeProps
|
||||
} = props;
|
||||
|
||||
const handleFilterChange = throttle(async (value: string) => {
|
||||
|
@ -81,27 +85,27 @@ export const RecycleTreeFilterDecorator: FilterHoc<
|
|||
};
|
||||
|
||||
const filterTreeReadyHandle = (api: IRecycleTreeHandle) => {
|
||||
onReady && onReady({
|
||||
...api,
|
||||
clearFilter: () => {
|
||||
setFilter('');
|
||||
setValue('');
|
||||
},
|
||||
} as IRecycleTreeFilterHandle);
|
||||
onReady &&
|
||||
onReady({
|
||||
...api,
|
||||
clearFilter: () => {
|
||||
setFilter('');
|
||||
setValue('');
|
||||
},
|
||||
} as IRecycleTreeFilterHandle);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
filterEnabled && (
|
||||
<FilterInput
|
||||
afterClear={filterAfterClear}
|
||||
placeholder={filterPlaceholder}
|
||||
value={value}
|
||||
autoFocus={filterAutoFocus}
|
||||
onValueChange={handleFilterInputChange} />
|
||||
)
|
||||
}
|
||||
{filterEnabled && (
|
||||
<FilterInput
|
||||
afterClear={filterAfterClear}
|
||||
placeholder={filterPlaceholder}
|
||||
value={value}
|
||||
autoFocus={filterAutoFocus}
|
||||
onValueChange={handleFilterInputChange}
|
||||
/>
|
||||
)}
|
||||
{React.createElement(recycleTreeComp, {
|
||||
...recycleTreeProps,
|
||||
height: height - (filterEnabled ? FILTER_AREA_HEIGHT : 0),
|
||||
|
|
|
@ -24,10 +24,10 @@ export abstract class PromptHandle {
|
|||
public readonly $addonAfter: HTMLDivElement;
|
||||
public readonly ProxiedInput: (props: ProxiedInputProp) => JSX.Element;
|
||||
private disposables: DisposableCollection = new DisposableCollection();
|
||||
private isInPendingCommitState: boolean = false;
|
||||
private _destroyed: boolean = false;
|
||||
private _hasValidateElement: boolean = false;
|
||||
private _hasAddonAfter: boolean = false;
|
||||
private isInPendingCommitState = false;
|
||||
private _destroyed = false;
|
||||
private _hasValidateElement = false;
|
||||
private _hasAddonAfter = false;
|
||||
private _validateClassName: string;
|
||||
|
||||
// event
|
||||
|
@ -60,9 +60,9 @@ export abstract class PromptHandle {
|
|||
// 可能存在PromptHandle创建后没被使用的情况
|
||||
}
|
||||
|
||||
abstract get id(): number
|
||||
abstract get id(): number;
|
||||
|
||||
abstract get depth(): number
|
||||
abstract get depth(): number;
|
||||
|
||||
get destroyed() {
|
||||
return this._destroyed;
|
||||
|
@ -174,11 +174,11 @@ export abstract class PromptHandle {
|
|||
|
||||
private handleClick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private handleKeyup = (ev) => {
|
||||
this.onChangeEmitter.fire(this.$.value);
|
||||
}
|
||||
};
|
||||
|
||||
private handleKeydown = async (ev) => {
|
||||
if (ev.key === 'Escape') {
|
||||
|
@ -205,11 +205,11 @@ export abstract class PromptHandle {
|
|||
this.$.disabled = false;
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleFocus = () => {
|
||||
this.onFocusEmitter.fire(this.$.value);
|
||||
}
|
||||
};
|
||||
|
||||
private handleBlur = async (ev) => {
|
||||
// 如果Input由于`react-virtualized`被从视图中卸载,在下一帧前Input的isConnected属性不会被更新
|
||||
|
@ -228,5 +228,5 @@ export abstract class PromptHandle {
|
|||
if (!this.isInPendingCommitState) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export abstract class Tree implements ITree {
|
|||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
protected nodes: {
|
||||
[id: string]: TreeNode | undefined,
|
||||
[id: string]: TreeNode | undefined;
|
||||
} = {};
|
||||
|
||||
get onNodeRefreshed() {
|
||||
|
@ -39,12 +39,8 @@ export abstract class Tree implements ITree {
|
|||
|
||||
sortComparator(a: ITreeNodeOrCompositeTreeNode, b: ITreeNodeOrCompositeTreeNode): number {
|
||||
if (a.constructor === b.constructor) {
|
||||
return a.name > b.name ? 1
|
||||
: a.name < b.name ? -1
|
||||
: 0;
|
||||
return a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
|
||||
}
|
||||
return CompositeTreeNode.is(a) ? -1
|
||||
: CompositeTreeNode.is(b) ? 1
|
||||
: 0;
|
||||
return CompositeTreeNode.is(a) ? -1 : CompositeTreeNode.is(b) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,20 @@
|
|||
import { IWatcherCallback, IWatchTerminator, IWatcherInfo, ITreeNodeOrCompositeTreeNode, ITreeNode, ICompositeTreeNode, TreeNodeEvent, IWatcherEvent, MetadataChangeType, ITreeWatcher, IMetadataChange, ITree, WatchEvent, TreeNodeType, IAccessibilityInformation } from '../types';
|
||||
import {
|
||||
IWatcherCallback,
|
||||
IWatchTerminator,
|
||||
IWatcherInfo,
|
||||
ITreeNodeOrCompositeTreeNode,
|
||||
ITreeNode,
|
||||
ICompositeTreeNode,
|
||||
TreeNodeEvent,
|
||||
IWatcherEvent,
|
||||
MetadataChangeType,
|
||||
ITreeWatcher,
|
||||
IMetadataChange,
|
||||
ITree,
|
||||
WatchEvent,
|
||||
TreeNodeType,
|
||||
IAccessibilityInformation,
|
||||
} from '../types';
|
||||
import { Event, Emitter, DisposableCollection, Path } from '../../utils';
|
||||
|
||||
/**
|
||||
|
@ -9,7 +25,7 @@ import { Event, Emitter, DisposableCollection, Path } from '../../utils';
|
|||
* @param deleteCount 删除或替换位置
|
||||
* @param items 插入的数组
|
||||
*/
|
||||
export function spliceArray(arr: number[], start: number, deleteCount: number = 0, items?: number[] | null) {
|
||||
export function spliceArray(arr: number[], start: number, deleteCount = 0, items?: number[] | null) {
|
||||
const a = arr.slice(0);
|
||||
a.splice(start, deleteCount, ...(items || []));
|
||||
return a;
|
||||
|
@ -53,7 +69,7 @@ export class TreeNode implements ITreeNode {
|
|||
private _parent: ICompositeTreeNode | undefined;
|
||||
|
||||
private _metadata: {
|
||||
[key: string]: any,
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
private _disposed: boolean;
|
||||
|
@ -62,7 +78,13 @@ export class TreeNode implements ITreeNode {
|
|||
protected _tree: ITree;
|
||||
protected _visible: boolean;
|
||||
|
||||
protected constructor(tree: ITree, parent?: ICompositeTreeNode, watcher?: ITreeWatcher, optionalMetadata?: { [key: string]: any }, options?: { disableCache?: boolean }) {
|
||||
protected constructor(
|
||||
tree: ITree,
|
||||
parent?: ICompositeTreeNode,
|
||||
watcher?: ITreeWatcher,
|
||||
optionalMetadata?: { [key: string]: any },
|
||||
options?: { disableCache?: boolean },
|
||||
) {
|
||||
this._uid = TreeNode.nextId();
|
||||
this._parent = parent;
|
||||
this._tree = tree;
|
||||
|
@ -101,7 +123,7 @@ export class TreeNode implements ITreeNode {
|
|||
|
||||
get whenReady() {
|
||||
// 保障节点是否完成的标识位
|
||||
return (async () => { })();
|
||||
return (async () => {})();
|
||||
}
|
||||
|
||||
get type() {
|
||||
|
@ -146,7 +168,12 @@ export class TreeNode implements ITreeNode {
|
|||
public addMetadata(withKey: string, value: any) {
|
||||
if (!(withKey in this._metadata)) {
|
||||
this._metadata[withKey] = value;
|
||||
this._watcher.notifyDidChangeMetadata(this, { type: MetadataChangeType.Added, key: withKey, prevValue: void 0, value });
|
||||
this._watcher.notifyDidChangeMetadata(this, {
|
||||
type: MetadataChangeType.Added,
|
||||
key: withKey,
|
||||
prevValue: void 0,
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
const prevValue = this._metadata[withKey];
|
||||
this._metadata[withKey] = value;
|
||||
|
@ -158,7 +185,12 @@ export class TreeNode implements ITreeNode {
|
|||
if (withKey in this._metadata) {
|
||||
const prevValue = this._metadata[withKey];
|
||||
delete this._metadata[withKey];
|
||||
this._watcher.notifyDidChangeMetadata(this, { type: MetadataChangeType.Removed, key: withKey, prevValue, value: void 0 });
|
||||
this._watcher.notifyDidChangeMetadata(this, {
|
||||
type: MetadataChangeType.Removed,
|
||||
key: withKey,
|
||||
prevValue,
|
||||
value: void 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,7 +245,9 @@ export class TreeNode implements ITreeNode {
|
|||
}
|
||||
|
||||
protected dispose() {
|
||||
if (this._disposed) { return; }
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
this._watcher.notifyWillDispose(this);
|
||||
TreeNode.removeTreeNode(this._uid, this.path);
|
||||
this._watcher.notifyDidDispose(this);
|
||||
|
@ -222,16 +256,11 @@ export class TreeNode implements ITreeNode {
|
|||
}
|
||||
|
||||
export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
||||
|
||||
private static defaultSortComparator(a: ITreeNodeOrCompositeTreeNode, b: ITreeNodeOrCompositeTreeNode): number {
|
||||
if (a.constructor === b.constructor) {
|
||||
return a.name > b.name ? 1
|
||||
: a.name < b.name ? -1
|
||||
: 0;
|
||||
return a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
|
||||
}
|
||||
return CompositeTreeNode.is(a) ? -1
|
||||
: CompositeTreeNode.is(b) ? 1
|
||||
: 0;
|
||||
return CompositeTreeNode.is(a) ? -1 : CompositeTreeNode.is(b) ? 1 : 0;
|
||||
}
|
||||
|
||||
public static is(node: any): node is ICompositeTreeNode {
|
||||
|
@ -250,7 +279,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
private hardReloadPromise: Promise<void> | null;
|
||||
private hardReloadPResolver: (() => void) | null;
|
||||
|
||||
private refreshTasks: (string[])[] = [];
|
||||
private refreshTasks: string[][] = [];
|
||||
private activeRefreshPromise: Promise<any> | null;
|
||||
private queuedRefreshPromise: Promise<any> | null;
|
||||
private queuedRefreshPromiseFactory: (() => Promise<any>) | null;
|
||||
|
@ -269,10 +298,18 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
notifyWillProcessWatchEvent: (target: ICompositeTreeNode, event: IWatcherEvent) => {
|
||||
emitter.fire({ type: TreeNodeEvent.WillProcessWatchEvent, args: [target, event] });
|
||||
},
|
||||
notifyWillChangeParent: (target: ITreeNodeOrCompositeTreeNode, prevParent: ICompositeTreeNode, newParent: ICompositeTreeNode) => {
|
||||
notifyWillChangeParent: (
|
||||
target: ITreeNodeOrCompositeTreeNode,
|
||||
prevParent: ICompositeTreeNode,
|
||||
newParent: ICompositeTreeNode,
|
||||
) => {
|
||||
emitter.fire({ type: TreeNodeEvent.WillChangeParent, args: [target, prevParent, newParent] });
|
||||
},
|
||||
notifyDidChangeParent: (target: ITreeNodeOrCompositeTreeNode, prevParent: ICompositeTreeNode, newParent: ICompositeTreeNode) => {
|
||||
notifyDidChangeParent: (
|
||||
target: ITreeNodeOrCompositeTreeNode,
|
||||
prevParent: ICompositeTreeNode,
|
||||
newParent: ICompositeTreeNode,
|
||||
) => {
|
||||
emitter.fire({ type: TreeNodeEvent.DidChangeParent, args: [target, prevParent, newParent] });
|
||||
},
|
||||
notifyWillDispose: (target: ITreeNodeOrCompositeTreeNode) => {
|
||||
|
@ -329,9 +366,15 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
|
||||
// parent 为undefined即表示该节点为根节点
|
||||
constructor(tree: ITree, parent: ICompositeTreeNode | undefined, watcher?: ITreeWatcher, optionalMetadata?: { [key: string]: any }, options?: { disableCache?: boolean }) {
|
||||
constructor(
|
||||
tree: ITree,
|
||||
parent: ICompositeTreeNode | undefined,
|
||||
watcher?: ITreeWatcher,
|
||||
optionalMetadata?: { [key: string]: any },
|
||||
options?: { disableCache?: boolean },
|
||||
) {
|
||||
super(tree, parent, watcher, optionalMetadata, options);
|
||||
this.isExpanded = !!parent ? false : true;
|
||||
this.isExpanded = parent ? false : true;
|
||||
this._branchSize = 0;
|
||||
if (!parent) {
|
||||
this.watchEvents = new Map();
|
||||
|
@ -416,7 +459,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
|
||||
// 展开节点
|
||||
public async setExpanded(ensureVisible: boolean = true, quiet: boolean = false) {
|
||||
public async setExpanded(ensureVisible = true, quiet = false) {
|
||||
// 根节点不可折叠
|
||||
if (CompositeTreeNode.isRoot(this)) {
|
||||
return;
|
||||
|
@ -450,7 +493,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
// 获取当前节点下所有展开的节点路径
|
||||
private getAllExpandedNodePath() {
|
||||
let paths: string[] = [];
|
||||
if (!!this.children) {
|
||||
if (this.children) {
|
||||
for (const child of this.children) {
|
||||
if ((child as CompositeTreeNode).isExpanded) {
|
||||
paths.push(child.path);
|
||||
|
@ -466,7 +509,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
// 获取当前节点下所有折叠的节点路径
|
||||
private getAllCollapsedNodePath() {
|
||||
let paths: string[] = [];
|
||||
if (!!this.children) {
|
||||
if (this.children) {
|
||||
for (const child of this.children) {
|
||||
if (!CompositeTreeNode.is(child)) {
|
||||
continue;
|
||||
|
@ -484,13 +527,13 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
|
||||
// 静默刷新子节点, 即不触发分支更新事件
|
||||
private async forceReloadChildrenQuiet(expandedPaths: string[] = this.getAllExpandedNodePath(), needReload: boolean = true) {
|
||||
private async forceReloadChildrenQuiet(expandedPaths: string[] = this.getAllExpandedNodePath(), needReload = true) {
|
||||
let forceLoadPath;
|
||||
if (this.isExpanded) {
|
||||
if (needReload) {
|
||||
await this.hardReloadChildren(true);
|
||||
}
|
||||
while (forceLoadPath = expandedPaths.shift()) {
|
||||
while ((forceLoadPath = expandedPaths.shift())) {
|
||||
const relativePath = new Path(this.path).relative(new Path(forceLoadPath));
|
||||
if (!relativePath) {
|
||||
break;
|
||||
|
@ -515,7 +558,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
// 不需要重新reload压缩节点的子节点内容
|
||||
await (child as CompositeTreeNode).forceReloadChildrenQuiet(expandedPaths, false);
|
||||
} else {
|
||||
(child as CompositeTreeNode).expandBranch((child as CompositeTreeNode), true);
|
||||
(child as CompositeTreeNode).expandBranch(child as CompositeTreeNode, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -550,7 +593,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
} else {
|
||||
// 仅需处理存在子节点的情况,否则将会影响刷新后的节点长度
|
||||
if (!!this.children) {
|
||||
if (this.children) {
|
||||
// 清理子节点,等待下次展开时更新
|
||||
if (!!this.children && this.parent) {
|
||||
for (const child of this.children) {
|
||||
|
@ -568,9 +611,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
if (!CompositeTreeNode.isRoot(this)) {
|
||||
return;
|
||||
}
|
||||
collapsedPaths = collapsedPaths.sort((a, b) => {
|
||||
return Path.pathDepth(a) - Path.pathDepth(b);
|
||||
});
|
||||
collapsedPaths = collapsedPaths.sort((a, b) => Path.pathDepth(a) - Path.pathDepth(b));
|
||||
let path;
|
||||
while (collapsedPaths.length > 0) {
|
||||
path = collapsedPaths.pop();
|
||||
|
@ -588,9 +629,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
if (!CompositeTreeNode.isRoot(this)) {
|
||||
return;
|
||||
}
|
||||
expandedPaths = expandedPaths.sort((a, b) => {
|
||||
return Path.pathDepth(a) - Path.pathDepth(b);
|
||||
});
|
||||
expandedPaths = expandedPaths.sort((a, b) => Path.pathDepth(a) - Path.pathDepth(b));
|
||||
let path;
|
||||
while (expandedPaths.length > 0) {
|
||||
path = expandedPaths.pop();
|
||||
|
@ -604,7 +643,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
|
||||
// 折叠节点
|
||||
public setCollapsed(quiet: boolean = false) {
|
||||
public setCollapsed(quiet = false) {
|
||||
// 根节点不可折叠
|
||||
if (CompositeTreeNode.isRoot(this)) {
|
||||
return;
|
||||
|
@ -656,7 +695,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
const branchSizeIncrease = 1 + ((item instanceof CompositeTreeNode && item.expanded) ? item._branchSize : 0);
|
||||
const branchSizeIncrease = 1 + (item instanceof CompositeTreeNode && item.expanded ? item._branchSize : 0);
|
||||
if (this._children) {
|
||||
this._children.push(item);
|
||||
this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
|
||||
|
@ -678,7 +717,9 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
const leadingSibling = this._children![relativeInsertionIndex - 1];
|
||||
if (leadingSibling) {
|
||||
const siblingIdx = master._flattenedBranch.indexOf(leadingSibling.id);
|
||||
relativeInsertionIndex = siblingIdx + ((leadingSibling instanceof CompositeTreeNode && leadingSibling.expanded) ? leadingSibling._branchSize : 0);
|
||||
relativeInsertionIndex =
|
||||
siblingIdx +
|
||||
(leadingSibling instanceof CompositeTreeNode && leadingSibling.expanded ? leadingSibling._branchSize : 0);
|
||||
} else {
|
||||
relativeInsertionIndex = master._flattenedBranch.indexOf(this.id);
|
||||
}
|
||||
|
@ -720,7 +761,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
return;
|
||||
}
|
||||
this._children!.splice(idx, 1);
|
||||
const branchSizeDecrease = 1 + ((item instanceof CompositeTreeNode && item.expanded) ? item._branchSize : 0);
|
||||
const branchSizeDecrease = 1 + (item instanceof CompositeTreeNode && item.expanded ? item._branchSize : 0);
|
||||
this._branchSize -= branchSizeDecrease;
|
||||
// 逐级往上查找节点的父节点,并沿途裁剪分支数
|
||||
let master: CompositeTreeNode = this;
|
||||
|
@ -736,7 +777,9 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
|
||||
if (item instanceof CompositeTreeNode && item.expanded) {
|
||||
(item as CompositeTreeNode).setFlattenedBranch(master._flattenedBranch.slice(removalBeginIdx + 1, removalBeginIdx + branchSizeDecrease));
|
||||
(item as CompositeTreeNode).setFlattenedBranch(
|
||||
master._flattenedBranch.slice(removalBeginIdx + 1, removalBeginIdx + branchSizeDecrease),
|
||||
);
|
||||
}
|
||||
|
||||
master.setFlattenedBranch(spliceArray(master._flattenedBranch, removalBeginIdx, branchSizeDecrease));
|
||||
|
@ -763,7 +806,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
const newP = new Path(newPath);
|
||||
const to = newP.dir.toString();
|
||||
const destDir = to === from ? this : TreeNode.getTreeNodeByPath(to);
|
||||
if (!(CompositeTreeNode.is(destDir))) {
|
||||
if (!CompositeTreeNode.is(destDir)) {
|
||||
this.unlinkItem(item);
|
||||
return;
|
||||
}
|
||||
|
@ -822,7 +865,10 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
// 最终导致此处查询不到对应节点,下面的shrinkBranch同样可能有相同问题,如点击折叠全部功能时
|
||||
return;
|
||||
}
|
||||
this.setFlattenedBranch(spliceArray(this._flattenedBranch, injectionStartIdx, 0, branch._flattenedBranch), withoutNotify);
|
||||
this.setFlattenedBranch(
|
||||
spliceArray(this._flattenedBranch, injectionStartIdx, 0, branch._flattenedBranch),
|
||||
withoutNotify,
|
||||
);
|
||||
// 取消展开分支对于分支的所有权,即最终只会有顶部Root拥有所有分支信息
|
||||
branch.setFlattenedBranch(null, withoutNotify);
|
||||
} else if (this.parent) {
|
||||
|
@ -847,8 +893,18 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
return;
|
||||
}
|
||||
// 返回分支对于分支信息所有权,即将折叠的节点信息再次存储于折叠了的节点中
|
||||
branch.setFlattenedBranch(this._flattenedBranch.slice(removalStartIdx, removalStartIdx + branch._branchSize), withoutNotify);
|
||||
this.setFlattenedBranch(spliceArray(this._flattenedBranch, removalStartIdx, branch._flattenedBranch ? branch._flattenedBranch.length : 0), withoutNotify);
|
||||
branch.setFlattenedBranch(
|
||||
this._flattenedBranch.slice(removalStartIdx, removalStartIdx + branch._branchSize),
|
||||
withoutNotify,
|
||||
);
|
||||
this.setFlattenedBranch(
|
||||
spliceArray(
|
||||
this._flattenedBranch,
|
||||
removalStartIdx,
|
||||
branch._flattenedBranch ? branch._flattenedBranch.length : 0,
|
||||
),
|
||||
withoutNotify,
|
||||
);
|
||||
} else if (this.parent) {
|
||||
(this.parent as CompositeTreeNode).shrinkBranch(branch, withoutNotify);
|
||||
}
|
||||
|
@ -862,13 +918,13 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
if (this.hardReloadPromise) {
|
||||
return this.hardReloadPromise;
|
||||
}
|
||||
this.hardReloadPromise = new Promise((res) => this.hardReloadPResolver = res);
|
||||
this.hardReloadPromise = new Promise((res) => (this.hardReloadPResolver = res));
|
||||
this.hardReloadPromise.then(() => {
|
||||
this.hardReloadPromise = null;
|
||||
this.hardReloadPResolver = null;
|
||||
});
|
||||
|
||||
const rawItems = await this._tree.resolveChildren(this) || [];
|
||||
const rawItems = (await this._tree.resolveChildren(this)) || [];
|
||||
|
||||
if (this._children) {
|
||||
// 重置节点分支
|
||||
|
@ -909,15 +965,23 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
this.watcher.notifyWillProcessWatchEvent(this, event);
|
||||
if (event.type === WatchEvent.Moved) {
|
||||
const { oldPath, newPath } = event;
|
||||
if (typeof oldPath !== 'string') { throw new TypeError(`Expected oldPath to be a string`); }
|
||||
if (typeof newPath !== 'string') { throw new TypeError(`Expected newPath to be a string`); }
|
||||
if (Path.isRelative(oldPath)) { throw new TypeError(`oldPath must be absolute`); }
|
||||
if (Path.isRelative(newPath)) { throw new TypeError(`newPath must be absolute`); }
|
||||
if (typeof oldPath !== 'string') {
|
||||
throw new TypeError('Expected oldPath to be a string');
|
||||
}
|
||||
if (typeof newPath !== 'string') {
|
||||
throw new TypeError('Expected newPath to be a string');
|
||||
}
|
||||
if (Path.isRelative(oldPath)) {
|
||||
throw new TypeError('oldPath must be absolute');
|
||||
}
|
||||
if (Path.isRelative(newPath)) {
|
||||
throw new TypeError('newPath must be absolute');
|
||||
}
|
||||
this.transferItem(oldPath, newPath);
|
||||
} else if (event.type === WatchEvent.Added) {
|
||||
const { node } = event;
|
||||
if (!TreeNode.is(node)) {
|
||||
throw new TypeError(`Expected node to be a TreeNode`);
|
||||
throw new TypeError('Expected node to be a TreeNode');
|
||||
}
|
||||
this.insertItem(node);
|
||||
} else if (event.type === WatchEvent.Removed) {
|
||||
|
@ -935,7 +999,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
// 预存展开目录
|
||||
const expandedPaths = this.getAllExpandedNodePath();
|
||||
// Changed事件,表示节点有较多的变化时,重新更新当前Tree节点
|
||||
if (!!this.children) {
|
||||
if (this.children) {
|
||||
for (const child of this.children) {
|
||||
(child as CompositeTreeNode).dispose();
|
||||
}
|
||||
|
@ -950,7 +1014,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
}
|
||||
this.watcher.notifyDidProcessWatchEvent(this, event);
|
||||
}
|
||||
};
|
||||
|
||||
public async refresh(paths: string[] = this.getAllExpandedNodePath()) {
|
||||
this.refreshTasks.push(paths);
|
||||
|
@ -984,13 +1048,16 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
this.activeRefreshPromise = promiseFactory();
|
||||
|
||||
return new Promise((c, e) => {
|
||||
this.activeRefreshPromise!.then((result: any) => {
|
||||
this.activeRefreshPromise = null;
|
||||
c(result);
|
||||
}, (err: any) => {
|
||||
this.activeRefreshPromise = null;
|
||||
e(err);
|
||||
});
|
||||
this.activeRefreshPromise!.then(
|
||||
(result: any) => {
|
||||
this.activeRefreshPromise = null;
|
||||
c(result);
|
||||
},
|
||||
(err: any) => {
|
||||
this.activeRefreshPromise = null;
|
||||
e(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1000,7 +1067,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
return await this.forceReloadChildrenQuiet(paths);
|
||||
}
|
||||
|
||||
private mergeExpandedPaths(paths: (string[])[]) {
|
||||
private mergeExpandedPaths(paths: string[][]) {
|
||||
// 返回最长的刷新路径即可
|
||||
let result;
|
||||
for (const path of paths) {
|
||||
|
@ -1058,9 +1125,9 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
await this.ensureLoaded();
|
||||
let next = this._children;
|
||||
let preItem: CompositeTreeNode;
|
||||
let preItemPath: string = '';
|
||||
let preItemPath = '';
|
||||
let name;
|
||||
while (name = pathFlag.shift()) {
|
||||
while ((name = pathFlag.shift())) {
|
||||
let item = next!.find((c) => c.name.indexOf(name) === 0);
|
||||
if (item && pathFlag.length === 0) {
|
||||
return item;
|
||||
|
@ -1118,7 +1185,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
}
|
||||
next = (item as CompositeTreeNode)._children;
|
||||
preItem = (item as CompositeTreeNode);
|
||||
preItem = item as CompositeTreeNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1172,11 +1239,11 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
|
|||
}
|
||||
|
||||
/**
|
||||
* 根据节点路径获取节点
|
||||
* @param {string} path
|
||||
* @returns
|
||||
* @memberof CompositeTreeNode
|
||||
*/
|
||||
* 根据节点路径获取节点
|
||||
* @param {string} path
|
||||
* @returns
|
||||
* @memberof CompositeTreeNode
|
||||
*/
|
||||
public getTreeNodeByPath(path: string) {
|
||||
return TreeNode.getTreeNodeByPath(path);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export class ClasslistComposite {
|
|||
* 注销样式变化监听函数
|
||||
*/
|
||||
public readonly removeChangeListener: (namedCallback: () => void) => void,
|
||||
) { }
|
||||
) {}
|
||||
}
|
||||
|
||||
export enum CompositeDecorationType {
|
||||
|
@ -48,7 +48,8 @@ export class CompositeDecoration {
|
|||
|
||||
this.compositeCssClasslist = new ClasslistComposite(
|
||||
this.classlistChangeCallbacks.add.bind(this.classlistChangeCallbacks),
|
||||
this.classlistChangeCallbacks.delete.bind(this.classlistChangeCallbacks));
|
||||
this.classlistChangeCallbacks.delete.bind(this.classlistChangeCallbacks),
|
||||
);
|
||||
|
||||
if (parent) {
|
||||
this.selfOwned = false;
|
||||
|
@ -67,7 +68,7 @@ export class CompositeDecoration {
|
|||
|
||||
public changeParent(newParent?: CompositeDecoration) {
|
||||
if (!newParent) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
if (!this.selfOwned) {
|
||||
return this.parentOwn(newParent);
|
||||
|
@ -92,18 +93,28 @@ export class CompositeDecoration {
|
|||
public add(decoration: Decoration): void {
|
||||
const applicationMode = decoration.appliedTargets.get(this.target);
|
||||
|
||||
const applicableToSelf = applicationMode && (applicationMode === TargetMatchMode.Self || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
const applicableToChildren = applicationMode && (applicationMode === TargetMatchMode.Children || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
const applicableToSelf =
|
||||
applicationMode &&
|
||||
(applicationMode === TargetMatchMode.Self || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
const applicableToChildren =
|
||||
applicationMode &&
|
||||
(applicationMode === TargetMatchMode.Children || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
|
||||
if (this.type === CompositeDecorationType.Applicable && !applicableToSelf) { return; }
|
||||
if (this.type === CompositeDecorationType.Inheritable && !applicableToChildren) { return; }
|
||||
if (this.type === CompositeDecorationType.Applicable && !applicableToSelf) {
|
||||
return;
|
||||
}
|
||||
if (this.type === CompositeDecorationType.Inheritable && !applicableToChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selfOwned) {
|
||||
this.selfOwn(ChangeReason.TargetDecoration, decoration);
|
||||
this.targetedDecorations.add(decoration);
|
||||
return;
|
||||
}
|
||||
if (this.targetedDecorations.has(decoration)) { return; }
|
||||
if (this.targetedDecorations.has(decoration)) {
|
||||
return;
|
||||
}
|
||||
this.targetedDecorations.add(decoration);
|
||||
this.recursiveRefresh(this, false, ChangeReason.TargetDecoration, decoration);
|
||||
}
|
||||
|
@ -123,18 +134,26 @@ export class CompositeDecoration {
|
|||
public negate(decoration: Decoration): void {
|
||||
const negationMode = decoration.negatedTargets.get(this.target);
|
||||
|
||||
const negatedOnSelf = negationMode && (negationMode === TargetMatchMode.Self || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
const negatedOnChildren = negationMode && (negationMode === TargetMatchMode.Children || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
const negatedOnSelf =
|
||||
negationMode && (negationMode === TargetMatchMode.Self || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
const negatedOnChildren =
|
||||
negationMode && (negationMode === TargetMatchMode.Children || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
|
||||
if (this.type === CompositeDecorationType.Applicable && !negatedOnSelf) { return; }
|
||||
if (this.type === CompositeDecorationType.Inheritable && !negatedOnChildren) { return; }
|
||||
if (this.type === CompositeDecorationType.Applicable && !negatedOnSelf) {
|
||||
return;
|
||||
}
|
||||
if (this.type === CompositeDecorationType.Inheritable && !negatedOnChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selfOwned) {
|
||||
this.selfOwn(ChangeReason.UnTargetDecoration, decoration);
|
||||
this.negatedDecorations.add(decoration);
|
||||
return;
|
||||
}
|
||||
if (this.negatedDecorations.has(decoration)) { return; }
|
||||
if (this.negatedDecorations.has(decoration)) {
|
||||
return;
|
||||
}
|
||||
this.negatedDecorations.add(decoration);
|
||||
if (this.renderedDecorations.has(decoration)) {
|
||||
this.removeDecorationClasslist(decoration);
|
||||
|
@ -150,8 +169,10 @@ export class CompositeDecoration {
|
|||
return this.parentOwn();
|
||||
}
|
||||
// 当前非父节点并且其父节点和其本身均不含有该装饰器
|
||||
if (!this.renderedDecorations.has(decoration) &&
|
||||
(this.parent.renderedDecorations.has(decoration) || decoration.appliedTargets.has(this.target))) {
|
||||
if (
|
||||
!this.renderedDecorations.has(decoration) &&
|
||||
(this.parent.renderedDecorations.has(decoration) || decoration.appliedTargets.has(this.target))
|
||||
) {
|
||||
this.recursiveRefresh(this, false, ChangeReason.TargetDecoration, decoration);
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +180,7 @@ export class CompositeDecoration {
|
|||
|
||||
private selfOwn(reason: ChangeReason, decoration: Decoration) {
|
||||
if (this.selfOwned) {
|
||||
throw new Error(`CompositeDecoration is already self owned`);
|
||||
throw new Error('CompositeDecoration is already self owned');
|
||||
}
|
||||
const parent = this.parent;
|
||||
this.selfOwned = true;
|
||||
|
@ -176,11 +197,13 @@ export class CompositeDecoration {
|
|||
}
|
||||
|
||||
// 当触发的为:not类型装饰器变化
|
||||
if (reason === ChangeReason.UnTargetDecoration &&
|
||||
if (
|
||||
reason === ChangeReason.UnTargetDecoration &&
|
||||
// 父节点装饰器拥有此装饰器
|
||||
this.parent.renderedDecorations.has(decoration) &&
|
||||
// 本身不包含此装饰器
|
||||
!this.renderedDecorations.has(decoration)) {
|
||||
!this.renderedDecorations.has(decoration)
|
||||
) {
|
||||
// 通知ClassList变化
|
||||
this.notifyClasslistChange(false);
|
||||
}
|
||||
|
@ -205,19 +228,27 @@ export class CompositeDecoration {
|
|||
|
||||
private processCompositeAlteration(reason: ChangeReason, decoration: Decoration): boolean {
|
||||
if (!this.selfOwned) {
|
||||
throw new Error(`CompositeDecoration is not self owned`);
|
||||
throw new Error('CompositeDecoration is not self owned');
|
||||
}
|
||||
if (reason === ChangeReason.UnTargetDecoration) {
|
||||
const disposable = this.renderedDecorations.get(decoration);
|
||||
if (disposable) {
|
||||
const applicationMode = decoration.appliedTargets.get(this.target);
|
||||
|
||||
const applicableToSelf = applicationMode && (applicationMode === TargetMatchMode.Self || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
const applicableToChildren = applicationMode && (applicationMode === TargetMatchMode.Children || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
const applicableToSelf =
|
||||
applicationMode &&
|
||||
(applicationMode === TargetMatchMode.Self || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
const applicableToChildren =
|
||||
applicationMode &&
|
||||
(applicationMode === TargetMatchMode.Children || applicationMode === TargetMatchMode.SelfAndChildren);
|
||||
|
||||
if (applicableToSelf && this.type === CompositeDecorationType.Applicable) { return false; }
|
||||
if (applicableToSelf && this.type === CompositeDecorationType.Applicable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (applicableToChildren && this.type === CompositeDecorationType.Inheritable) { return false; }
|
||||
if (applicableToChildren && this.type === CompositeDecorationType.Inheritable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.removeDecorationClasslist(decoration, false);
|
||||
|
||||
|
@ -232,12 +263,18 @@ export class CompositeDecoration {
|
|||
if (reason === ChangeReason.TargetDecoration) {
|
||||
const negationMode = decoration.negatedTargets.get(this.target);
|
||||
|
||||
const negatedOnSelf = negationMode && (negationMode === TargetMatchMode.Self || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
const negatedOnChildren = negationMode && (negationMode === TargetMatchMode.Children || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
const negatedOnSelf =
|
||||
negationMode && (negationMode === TargetMatchMode.Self || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
const negatedOnChildren =
|
||||
negationMode && (negationMode === TargetMatchMode.Children || negationMode === TargetMatchMode.SelfAndChildren);
|
||||
|
||||
if (negatedOnSelf && this.type === CompositeDecorationType.Applicable) { return false; }
|
||||
if (negatedOnSelf && this.type === CompositeDecorationType.Applicable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (negatedOnChildren && this.type === CompositeDecorationType.Inheritable) { return false; }
|
||||
if (negatedOnChildren && this.type === CompositeDecorationType.Inheritable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.renderedDecorations.has(decoration)) {
|
||||
const disposables = new DisposableCollection();
|
||||
|
@ -257,7 +294,13 @@ export class CompositeDecoration {
|
|||
return false;
|
||||
}
|
||||
|
||||
private recursiveRefresh(origin: CompositeDecoration, updateReferences: boolean, reason?: ChangeReason, decoration?: Decoration, notifyListeners = true) {
|
||||
private recursiveRefresh(
|
||||
origin: CompositeDecoration,
|
||||
updateReferences: boolean,
|
||||
reason?: ChangeReason,
|
||||
decoration?: Decoration,
|
||||
notifyListeners = true,
|
||||
) {
|
||||
// 更改当前manager引用的renderedDecorations及compositeCssClasslist.classlist
|
||||
if (!this.selfOwned && updateReferences) {
|
||||
this.renderedDecorations = this.parent.renderedDecorations;
|
||||
|
@ -278,12 +321,23 @@ export class CompositeDecoration {
|
|||
if (notifyListeners) {
|
||||
this.notifyClasslistChange(false);
|
||||
}
|
||||
} else if (this.selfOwned && reason === ChangeReason.UnTargetDecoration && decoration && this.renderedDecorations.has(decoration)) {
|
||||
} else if (
|
||||
this.selfOwned &&
|
||||
reason === ChangeReason.UnTargetDecoration &&
|
||||
decoration &&
|
||||
this.renderedDecorations.has(decoration)
|
||||
) {
|
||||
this.processCompositeAlteration(reason, decoration);
|
||||
if (notifyListeners) {
|
||||
this.notifyClasslistChange(false);
|
||||
}
|
||||
} else if (this.selfOwned && reason === ChangeReason.TargetDecoration && decoration && this.processCompositeAlteration(reason, decoration) && notifyListeners) {
|
||||
} else if (
|
||||
this.selfOwned &&
|
||||
reason === ChangeReason.TargetDecoration &&
|
||||
decoration &&
|
||||
this.processCompositeAlteration(reason, decoration) &&
|
||||
notifyListeners
|
||||
) {
|
||||
this.notifyClasslistChange(false);
|
||||
} else if (!this.selfOwned && notifyListeners) {
|
||||
this.notifyClasslistChange(false);
|
||||
|
@ -298,41 +352,41 @@ export class CompositeDecoration {
|
|||
const { classname } = event;
|
||||
|
||||
if (!this.selfOwned || !classname) {
|
||||
return ;
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.compositeCssClasslist.classlist as string[]).push(classname);
|
||||
this.notifyClasslistChange();
|
||||
}
|
||||
};
|
||||
|
||||
private handleDecorationDidRemoveClassname = (event: IDecorationEventData) => {
|
||||
const { classname } = event;
|
||||
if (!this.selfOwned || !classname) {
|
||||
return ;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const idx = this.compositeCssClasslist.classlist.indexOf(classname);
|
||||
if (idx > -1) {
|
||||
(this.compositeCssClasslist.classlist as string[]).splice(idx, 1);
|
||||
this.notifyClasslistChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private mergeDecorationClasslist = (event: IDecorationEventData) => {
|
||||
const { decoration } = event;
|
||||
if (!this.selfOwned) {
|
||||
return ;
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.compositeCssClasslist.classlist as string[]).push(...decoration.cssClassList);
|
||||
this.notifyClasslistChange();
|
||||
}
|
||||
};
|
||||
|
||||
private handleDecorationDisable = (event: IDecorationEventData) => {
|
||||
const { decoration } = event;
|
||||
this.removeDecorationClasslist(decoration);
|
||||
}
|
||||
};
|
||||
private removeDecorationClasslist(decoration: Decoration, notifyAll = true) {
|
||||
if (!this.selfOwned) {
|
||||
return ;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const classname of decoration.cssClassList) {
|
||||
const idx = this.compositeCssClasslist.classlist.indexOf(classname);
|
||||
if (idx > -1) {
|
||||
|
|
|
@ -23,7 +23,6 @@ export interface IDecorationTargetChangeEventData {
|
|||
}
|
||||
|
||||
export class Decoration {
|
||||
|
||||
private _cssClassList: Set<string>;
|
||||
private _appliedTargets: Map<ITreeNode | ICompositeTreeNode, TargetMatchMode> = new Map();
|
||||
private _negatedTargets: Map<ITreeNode | ICompositeTreeNode, TargetMatchMode> = new Map();
|
||||
|
@ -61,9 +60,9 @@ export class Decoration {
|
|||
set disabled(disabled: boolean) {
|
||||
this._disabled = disabled;
|
||||
if (disabled) {
|
||||
this.onDidDisableDecorationEmitter.fire({decoration: this});
|
||||
this.onDidDisableDecorationEmitter.fire({ decoration: this });
|
||||
} else {
|
||||
this.onDidEnableDecorationEmitter.fire({decoration: this});
|
||||
this.onDidEnableDecorationEmitter.fire({ decoration: this });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,15 +111,19 @@ export class Decoration {
|
|||
}
|
||||
|
||||
public addCSSClass(className: string): void {
|
||||
if (this._cssClassList.has(className)) { return; }
|
||||
if (this._cssClassList.has(className)) {
|
||||
return;
|
||||
}
|
||||
this._cssClassList.add(className);
|
||||
this.onDidAddCSSClassnameEmitter.fire({decoration: this, classname: className});
|
||||
this.onDidAddCSSClassnameEmitter.fire({ decoration: this, classname: className });
|
||||
}
|
||||
|
||||
public removeCSSClass(className: string): void {
|
||||
if (!this._cssClassList.has(className)) { return; }
|
||||
if (!this._cssClassList.has(className)) {
|
||||
return;
|
||||
}
|
||||
this._cssClassList.delete(className);
|
||||
this.onDidRemoveCSSClassnameEmitter.fire({decoration: this, classname: className});
|
||||
this.onDidRemoveCSSClassnameEmitter.fire({ decoration: this, classname: className });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,16 +140,23 @@ export class Decoration {
|
|||
* @param target
|
||||
* @param flags
|
||||
*/
|
||||
public addTarget(target: ITreeNode | ICompositeTreeNode, flags: TargetMatchMode = TargetMatchMode.Self): IDisposable | undefined {
|
||||
public addTarget(
|
||||
target: ITreeNode | ICompositeTreeNode,
|
||||
flags: TargetMatchMode = TargetMatchMode.Self,
|
||||
): IDisposable | undefined {
|
||||
const existingFlags = this._appliedTargets.get(target);
|
||||
if (existingFlags === flags) { return; }
|
||||
if (!(TreeNode.is(target))) { return; }
|
||||
if (existingFlags === flags) {
|
||||
return;
|
||||
}
|
||||
if (!TreeNode.is(target)) {
|
||||
return;
|
||||
}
|
||||
this._appliedTargets.set(target, flags);
|
||||
const dispose = Disposable.create(() => {
|
||||
this.removeTarget(target);
|
||||
});
|
||||
this.appliedTargetsDisposables.set(target, dispose);
|
||||
this.onDidAddTargetEmitter.fire({decoration: this, target});
|
||||
this.appliedTargetsDisposables.set(target, dispose);
|
||||
this.onDidAddTargetEmitter.fire({ decoration: this, target });
|
||||
return dispose;
|
||||
}
|
||||
|
||||
|
@ -161,7 +171,7 @@ export class Decoration {
|
|||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this.onDidRemoveTargetEmitter.fire({decoration: this, target});
|
||||
this.onDidRemoveTargetEmitter.fire({ decoration: this, target });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,16 +182,23 @@ export class Decoration {
|
|||
* @param target
|
||||
* @param flags
|
||||
*/
|
||||
public negateTarget(target: ITreeNode | ICompositeTreeNode, flags: TargetMatchMode = TargetMatchMode.Self): IDisposable | undefined {
|
||||
const existingFlags = this._negatedTargets.get(target);
|
||||
if (existingFlags === flags) { return; }
|
||||
if (!(TreeNode.is(target))) { return; }
|
||||
public negateTarget(
|
||||
target: ITreeNode | ICompositeTreeNode,
|
||||
flags: TargetMatchMode = TargetMatchMode.Self,
|
||||
): IDisposable | undefined {
|
||||
const existingFlags = this._negatedTargets.get(target);
|
||||
if (existingFlags === flags) {
|
||||
return;
|
||||
}
|
||||
if (!TreeNode.is(target)) {
|
||||
return;
|
||||
}
|
||||
this._negatedTargets.set(target, flags);
|
||||
const dispose = Disposable.create(() => {
|
||||
this.unNegateTarget(target);
|
||||
});
|
||||
this.negatedTargetsDisposables.set(target, dispose);
|
||||
this.onDidNegateTargetEmitter.fire({decoration: this, target});
|
||||
this.negatedTargetsDisposables.set(target, dispose);
|
||||
this.onDidNegateTargetEmitter.fire({ decoration: this, target });
|
||||
return dispose;
|
||||
}
|
||||
|
||||
|
@ -190,12 +207,12 @@ export class Decoration {
|
|||
* @param target
|
||||
*/
|
||||
public unNegateTarget(target: ITreeNode | ICompositeTreeNode): void {
|
||||
if ( this._negatedTargets.delete(target)) {
|
||||
const disposable = this.negatedTargetsDisposables.get(target);
|
||||
if (this._negatedTargets.delete(target)) {
|
||||
const disposable = this.negatedTargetsDisposables.get(target);
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this.onDidUnNegateTargetEmitter.fire({decoration: this, target});
|
||||
this.onDidUnNegateTargetEmitter.fire({ decoration: this, target });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,10 +41,12 @@ export class DecorationsManager implements IDisposable {
|
|||
|
||||
public addDecoration(decoration: Decoration): void {
|
||||
if (this.disposed) {
|
||||
throw new Error(`DecorationManager disposed`);
|
||||
throw new Error('DecorationManager disposed');
|
||||
}
|
||||
|
||||
if (this.decorations.has(decoration)) { return; }
|
||||
if (this.decorations.has(decoration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disposable = new DisposableCollection();
|
||||
|
||||
|
@ -56,18 +58,20 @@ export class DecorationsManager implements IDisposable {
|
|||
this.decorations.set(decoration, disposable);
|
||||
|
||||
for (const [target] of decoration.appliedTargets) {
|
||||
this.targetDecoration({decoration, target});
|
||||
this.targetDecoration({ decoration, target });
|
||||
}
|
||||
|
||||
for (const [target] of decoration.negatedTargets) {
|
||||
this.negateDecoration({decoration, target});
|
||||
this.negateDecoration({ decoration, target });
|
||||
}
|
||||
}
|
||||
|
||||
public removeDecoration(decoration: Decoration): void {
|
||||
const decorationSubscriptions = this.decorations.get(decoration);
|
||||
|
||||
if (!decorationSubscriptions) { return; }
|
||||
if (!decorationSubscriptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [target] of decoration.appliedTargets) {
|
||||
const meta = this.decorationsMeta.get(target);
|
||||
|
@ -96,36 +100,40 @@ export class DecorationsManager implements IDisposable {
|
|||
}
|
||||
|
||||
public getDecorations(item: ITreeNodeOrCompositeTreeNode): ClasslistComposite | undefined {
|
||||
if (!item || (!TreeNode.is(item))) {
|
||||
return ;
|
||||
if (!item || !TreeNode.is(item)) {
|
||||
return;
|
||||
}
|
||||
const decMeta = this.getDecorationData(item);
|
||||
if (decMeta) {
|
||||
return decMeta.applicable.compositeCssClasslist;
|
||||
}
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
public getDecorationData(item: ITreeNodeOrCompositeTreeNode): IDecorationMeta | undefined {
|
||||
if (this.disposed) { return ; }
|
||||
if (this.disposed) {
|
||||
return;
|
||||
}
|
||||
const meta = this.decorationsMeta.get(item);
|
||||
if (meta) {
|
||||
return meta;
|
||||
}
|
||||
// 执行到这里说明该节点不是直接节点,而是需要从父级节点继承装饰器属性
|
||||
if (!item || !item.parent) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
const parentMeta = this.getDecorationData(item.parent as CompositeTreeNode);
|
||||
if (parentMeta) {
|
||||
const ownMeta: IDecorationMeta = {
|
||||
applicable: new CompositeDecoration(item, CompositeDecorationType.Applicable, parentMeta.inheritable),
|
||||
inheritable: CompositeTreeNode.is(item) ? new CompositeDecoration(item, CompositeDecorationType.Inheritable, parentMeta.inheritable) : undefined,
|
||||
inheritable: CompositeTreeNode.is(item)
|
||||
? new CompositeDecoration(item, CompositeDecorationType.Inheritable, parentMeta.inheritable)
|
||||
: undefined,
|
||||
};
|
||||
this.decorationsMeta.set(item, ownMeta);
|
||||
return ownMeta;
|
||||
}
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
private targetDecoration = (event: IDecorationTargetChangeEventData): void => {
|
||||
|
@ -140,7 +148,7 @@ export class DecorationsManager implements IDisposable {
|
|||
inheritable.add(decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private unTargetDecoration = (event: IDecorationTargetChangeEventData): void => {
|
||||
const { decoration, target } = event;
|
||||
|
@ -154,7 +162,7 @@ export class DecorationsManager implements IDisposable {
|
|||
inheritable.remove(decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private negateDecoration = (event: IDecorationTargetChangeEventData): void => {
|
||||
const { decoration, target } = event;
|
||||
|
@ -168,7 +176,7 @@ export class DecorationsManager implements IDisposable {
|
|||
inheritable.negate(decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private unNegateDecoration = (event: IDecorationTargetChangeEventData): void => {
|
||||
const { decoration, target } = event;
|
||||
|
@ -182,9 +190,13 @@ export class DecorationsManager implements IDisposable {
|
|||
inheritable.unNegate(decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private switchParent = (target: ITreeNodeOrCompositeTreeNode, prevParent: CompositeTreeNode, newParent: CompositeTreeNode): void => {
|
||||
private switchParent = (
|
||||
target: ITreeNodeOrCompositeTreeNode,
|
||||
prevParent: CompositeTreeNode,
|
||||
newParent: CompositeTreeNode,
|
||||
): void => {
|
||||
const ownMeta = this.decorationsMeta.get(target);
|
||||
if (!ownMeta) {
|
||||
return;
|
||||
|
@ -196,5 +208,5 @@ export class DecorationsManager implements IDisposable {
|
|||
ownMeta.inheritable.changeParent(newParentMeta.inheritable);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { CompositeTreeNode, TreeNode } from '../TreeNode';
|
|||
import { ICompositeTreeNode, TreeNodeEvent } from '../../types';
|
||||
|
||||
export class TreeModel {
|
||||
|
||||
private _state: TreeStateManager;
|
||||
private _root: CompositeTreeNode;
|
||||
|
||||
|
@ -43,7 +42,7 @@ export class TreeModel {
|
|||
|
||||
dispatchChange = () => {
|
||||
this.onChangeEmitter.fire();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据给定的状态信息还原Tree组件
|
||||
|
@ -81,12 +80,11 @@ export class TreeModel {
|
|||
*
|
||||
* `TreeStateWatcher#toString` 用于将当前状态转换为JSON字符串
|
||||
*/
|
||||
public getTreeStateWatcher(atSurfaceExpandedDirsOnly: boolean = false): TreeStateWatcher {
|
||||
public getTreeStateWatcher(atSurfaceExpandedDirsOnly = false): TreeStateWatcher {
|
||||
return new TreeStateWatcher(this.state, atSurfaceExpandedDirsOnly);
|
||||
}
|
||||
|
||||
protected resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
|
||||
return Promise.resolve(Array.from(parent.children!) as TreeNode[]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ export interface IExpansionStateChange {
|
|||
export class TreeStateManager {
|
||||
private root: CompositeTreeNode;
|
||||
private expandedDirectories: Map<CompositeTreeNode, string> = new Map();
|
||||
private _scrollOffset: number = 0;
|
||||
private stashing: boolean = false;
|
||||
private _scrollOffset = 0;
|
||||
private stashing = false;
|
||||
private stashKeyframes: Map<number, StashKeyFrameFlag> | null;
|
||||
private stashLockingItems: Set<TreeNode> = new Set();
|
||||
|
||||
|
@ -43,7 +43,6 @@ export class TreeStateManager {
|
|||
// 监听节点的折叠展开状态变化
|
||||
this.root.watcher.on(TreeNodeEvent.DidChangeExpansionState, this.handleExpansionChange);
|
||||
this.root.watcher.on(TreeNodeEvent.DidChangePath, this.handleDidChangePath);
|
||||
|
||||
}
|
||||
|
||||
get scrollOffset() {
|
||||
|
@ -76,10 +75,10 @@ export class TreeStateManager {
|
|||
for (const relPath of state.expandedDirectories.buried) {
|
||||
try {
|
||||
const node = await this.root.forceLoadTreeNodeAtPath(relPath);
|
||||
if (node && CompositeTreeNode.is(node) ) {
|
||||
if (node && CompositeTreeNode.is(node)) {
|
||||
await (node as CompositeTreeNode).setExpanded(false);
|
||||
}
|
||||
} catch (error) { }
|
||||
} catch (error) {}
|
||||
}
|
||||
for (const relPath of state.expandedDirectories.atSurface) {
|
||||
try {
|
||||
|
@ -87,9 +86,12 @@ export class TreeStateManager {
|
|||
if (node && CompositeTreeNode.is(node)) {
|
||||
await (node as CompositeTreeNode).setExpanded(true);
|
||||
}
|
||||
} catch (error) { }
|
||||
} catch (error) {}
|
||||
}
|
||||
this._scrollOffset = typeof state.scrollPosition === 'number' && state.scrollPosition > -1 ? state.scrollPosition : this._scrollOffset;
|
||||
this._scrollOffset =
|
||||
typeof state.scrollPosition === 'number' && state.scrollPosition > -1
|
||||
? state.scrollPosition
|
||||
: this._scrollOffset;
|
||||
this.onDidLoadStateEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +101,11 @@ export class TreeStateManager {
|
|||
*/
|
||||
public excludeFromStash(file: ITreeNodeOrCompositeTreeNode) {
|
||||
if (this.stashKeyframes && !this.stashing) {
|
||||
this.handleExpansionChange(!CompositeTreeNode.is(file) ? file.parent as CompositeTreeNode : file as CompositeTreeNode, true, this.root.isItemVisibleAtSurface(file));
|
||||
this.handleExpansionChange(
|
||||
!CompositeTreeNode.is(file) ? (file.parent as CompositeTreeNode) : (file as CompositeTreeNode),
|
||||
true,
|
||||
this.root.isItemVisibleAtSurface(file),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,7 +125,6 @@ export class TreeStateManager {
|
|||
while (p) {
|
||||
if (this.stashKeyframes.has(p.id)) {
|
||||
let flags = this.stashKeyframes.get(p.id) as StashKeyFrameFlag;
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
flags = flags | StashKeyFrameFlag.Disabled;
|
||||
this.stashKeyframes.set(p.id, flags as StashKeyFrameFlag);
|
||||
}
|
||||
|
@ -132,7 +137,6 @@ export class TreeStateManager {
|
|||
while (p) {
|
||||
if (this.stashKeyframes.has(p.id)) {
|
||||
let flags = this.stashKeyframes.get(p.id) as StashKeyFrameFlag;
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
flags &= ~StashKeyFrameFlag.Disabled;
|
||||
this.stashKeyframes.set(p.id, flags);
|
||||
}
|
||||
|
@ -145,21 +149,21 @@ export class TreeStateManager {
|
|||
if (isExpanded && !relativePath) {
|
||||
relativePath = new Path(this.root.path).relative(new Path(target.path))?.toString() as string;
|
||||
this.expandedDirectories.set(target, relativePath);
|
||||
this.onDidChangeExpansionStateEmitter.fire({relativePath, isExpanded, isVisibleAtSurface});
|
||||
this.onDidChangeExpansionStateEmitter.fire({ relativePath, isExpanded, isVisibleAtSurface });
|
||||
} else if (!isExpanded && relativePath) {
|
||||
this.expandedDirectories.delete(target);
|
||||
this.onDidChangeExpansionStateEmitter.fire({relativePath, isExpanded, isVisibleAtSurface});
|
||||
this.onDidChangeExpansionStateEmitter.fire({ relativePath, isExpanded, isVisibleAtSurface });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleDidChangePath = (target: CompositeTreeNode) => {
|
||||
if (this.expandedDirectories.has(target)) {
|
||||
const prevPath = this.expandedDirectories.get(target) as string;
|
||||
const newPath = new Path(this.root.path).relative(new Path(target.path))?.toString() as string;
|
||||
this.expandedDirectories.set(target, newPath);
|
||||
this.onDidChangeRelativePathEmitter.fire({prevPath, newPath});
|
||||
this.onDidChangeRelativePathEmitter.fire({ prevPath, newPath });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始记录点
|
||||
|
@ -191,17 +195,14 @@ export class TreeStateManager {
|
|||
const keyframes = Array.from(this.stashKeyframes);
|
||||
this.stashKeyframes = null;
|
||||
for (const [targetID, flags] of keyframes) {
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const frameDisabled = (flags & StashKeyFrameFlag.Disabled) === StashKeyFrameFlag.Disabled;
|
||||
const target: CompositeTreeNode = TreeNode.getTreeNodeById(targetID) as CompositeTreeNode;
|
||||
// 判断当前操作对象是否有效,无效则做下一步操作
|
||||
if (!target || frameDisabled) {
|
||||
continue;
|
||||
}
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
if ((flags & StashKeyFrameFlag.Expanded) === StashKeyFrameFlag.Expanded) {
|
||||
target.setCollapsed();
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
} else if ((flags & StashKeyFrameFlag.Collapsed) === StashKeyFrameFlag.Collapsed) {
|
||||
await target.setExpanded();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TreeStateManager, IPathChange, IExpansionStateChange } from './TreeStat
|
|||
import { ISerializableState, TreeStateWatcherChangeType as TreeStateChangeType } from './types';
|
||||
|
||||
export class TreeStateWatcher implements IDisposable {
|
||||
private _disposed: boolean = false;
|
||||
private _disposed = false;
|
||||
private disposables: DisposableCollection = new DisposableCollection();
|
||||
|
||||
private onDidTreeStateChangeEmitter: Emitter<TreeStateChangeType> = new Emitter();
|
||||
|
@ -17,75 +17,78 @@ export class TreeStateWatcher implements IDisposable {
|
|||
},
|
||||
};
|
||||
|
||||
constructor(
|
||||
treeState: TreeStateManager,
|
||||
atSurfaceExpandedDirsOnly: boolean = false,
|
||||
) {
|
||||
this.disposables.push(treeState.onChangeScrollOffset((newOffset: number) => {
|
||||
this.currentState.scrollPosition = newOffset;
|
||||
this.onDidTreeStateChangeEmitter.fire(TreeStateChangeType.ScrollOffset);
|
||||
}));
|
||||
this.disposables.push(treeState.onDidChangeRelativePath(({prevPath, newPath}: IPathChange) => {
|
||||
let shouldNotify = false;
|
||||
const surfaceSets = new Set(this.currentState.expandedDirectories.atSurface);
|
||||
constructor(treeState: TreeStateManager, atSurfaceExpandedDirsOnly = false) {
|
||||
this.disposables.push(
|
||||
treeState.onChangeScrollOffset((newOffset: number) => {
|
||||
this.currentState.scrollPosition = newOffset;
|
||||
this.onDidTreeStateChangeEmitter.fire(TreeStateChangeType.ScrollOffset);
|
||||
}),
|
||||
);
|
||||
this.disposables.push(
|
||||
treeState.onDidChangeRelativePath(({ prevPath, newPath }: IPathChange) => {
|
||||
let shouldNotify = false;
|
||||
const surfaceSets = new Set(this.currentState.expandedDirectories.atSurface);
|
||||
|
||||
if (surfaceSets.has(prevPath)) {
|
||||
surfaceSets.delete(prevPath);
|
||||
surfaceSets.add(newPath);
|
||||
shouldNotify = true;
|
||||
}
|
||||
this.currentState.expandedDirectories.atSurface = Array.from(surfaceSets);
|
||||
|
||||
if (atSurfaceExpandedDirsOnly) {
|
||||
const buriedSets = new Set(this.currentState.expandedDirectories.buried);
|
||||
if (buriedSets.has(prevPath)) {
|
||||
if (surfaceSets.has(prevPath)) {
|
||||
surfaceSets.delete(prevPath);
|
||||
surfaceSets.add(newPath);
|
||||
shouldNotify = true;
|
||||
}
|
||||
this.currentState.expandedDirectories.buried = Array.from(buriedSets);
|
||||
}
|
||||
if (shouldNotify) {
|
||||
this.onDidTreeStateChangeEmitter.fire(TreeStateChangeType.PathsUpdated);
|
||||
}
|
||||
}));
|
||||
this.currentState.expandedDirectories.atSurface = Array.from(surfaceSets);
|
||||
|
||||
this.disposables.push(treeState.onDidChangeExpansionState(({relativePath, isExpanded, isVisibleAtSurface}: IExpansionStateChange) => {
|
||||
let shouldNotify = false;
|
||||
const surfaceSets = new Set(this.currentState.expandedDirectories.atSurface);
|
||||
const buriedSets = new Set(this.currentState.expandedDirectories.buried);
|
||||
if (surfaceSets.has(relativePath) && (!isExpanded || !isVisibleAtSurface)) {
|
||||
surfaceSets.delete(relativePath);
|
||||
// 该目录下的子目录需要变为buried状态
|
||||
const restSurfaceArray = Array.from(surfaceSets);
|
||||
const pathsShouldBeBuried = restSurfaceArray.filter((rest) => rest.indexOf(relativePath) >= 0);
|
||||
for (const path of pathsShouldBeBuried) {
|
||||
surfaceSets.delete(path);
|
||||
if (!buriedSets.has(path)) {
|
||||
buriedSets.add(path);
|
||||
if (atSurfaceExpandedDirsOnly) {
|
||||
const buriedSets = new Set(this.currentState.expandedDirectories.buried);
|
||||
if (buriedSets.has(prevPath)) {
|
||||
surfaceSets.delete(prevPath);
|
||||
surfaceSets.add(newPath);
|
||||
shouldNotify = true;
|
||||
}
|
||||
this.currentState.expandedDirectories.buried = Array.from(buriedSets);
|
||||
}
|
||||
shouldNotify = true;
|
||||
} else if (isExpanded && isVisibleAtSurface) {
|
||||
surfaceSets.add(relativePath);
|
||||
shouldNotify = true;
|
||||
}
|
||||
this.currentState.expandedDirectories.atSurface = Array.from(surfaceSets);
|
||||
if (shouldNotify) {
|
||||
this.onDidTreeStateChangeEmitter.fire(TreeStateChangeType.PathsUpdated);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (!atSurfaceExpandedDirsOnly) {
|
||||
if (buriedSets.has(relativePath) && (!isExpanded || isVisibleAtSurface)) {
|
||||
buriedSets.delete(relativePath);
|
||||
this.disposables.push(
|
||||
treeState.onDidChangeExpansionState(({ relativePath, isExpanded, isVisibleAtSurface }: IExpansionStateChange) => {
|
||||
let shouldNotify = false;
|
||||
const surfaceSets = new Set(this.currentState.expandedDirectories.atSurface);
|
||||
const buriedSets = new Set(this.currentState.expandedDirectories.buried);
|
||||
if (surfaceSets.has(relativePath) && (!isExpanded || !isVisibleAtSurface)) {
|
||||
surfaceSets.delete(relativePath);
|
||||
// 该目录下的子目录需要变为buried状态
|
||||
const restSurfaceArray = Array.from(surfaceSets);
|
||||
const pathsShouldBeBuried = restSurfaceArray.filter((rest) => rest.indexOf(relativePath) >= 0);
|
||||
for (const path of pathsShouldBeBuried) {
|
||||
surfaceSets.delete(path);
|
||||
if (!buriedSets.has(path)) {
|
||||
buriedSets.add(path);
|
||||
}
|
||||
}
|
||||
shouldNotify = true;
|
||||
} else if (isExpanded && !isVisibleAtSurface) {
|
||||
buriedSets.add(relativePath);
|
||||
} else if (isExpanded && isVisibleAtSurface) {
|
||||
surfaceSets.add(relativePath);
|
||||
shouldNotify = true;
|
||||
}
|
||||
this.currentState.expandedDirectories.buried = Array.from(buriedSets);
|
||||
}
|
||||
if (shouldNotify) {
|
||||
this.onDidTreeStateChangeEmitter.fire(TreeStateChangeType.DirExpansionState);
|
||||
}
|
||||
}));
|
||||
this.currentState.expandedDirectories.atSurface = Array.from(surfaceSets);
|
||||
|
||||
if (!atSurfaceExpandedDirsOnly) {
|
||||
if (buriedSets.has(relativePath) && (!isExpanded || isVisibleAtSurface)) {
|
||||
buriedSets.delete(relativePath);
|
||||
shouldNotify = true;
|
||||
} else if (isExpanded && !isVisibleAtSurface) {
|
||||
buriedSets.add(relativePath);
|
||||
shouldNotify = true;
|
||||
}
|
||||
this.currentState.expandedDirectories.buried = Array.from(buriedSets);
|
||||
}
|
||||
if (shouldNotify) {
|
||||
this.onDidTreeStateChangeEmitter.fire(TreeStateChangeType.DirExpansionState);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
get onDidChange(): Event<TreeStateChangeType> {
|
||||
|
|
|
@ -2,8 +2,8 @@ export interface ISerializableState {
|
|||
specVersion: number;
|
||||
scrollPosition: number;
|
||||
expandedDirectories: {
|
||||
atSurface: string[],
|
||||
buried: string[],
|
||||
atSurface: string[];
|
||||
buried: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ITreeNodeOrCompositeTreeNode, ICompositeTreeNode } from './tree-node';
|
|||
|
||||
export interface ITree {
|
||||
// 加载子节点函数
|
||||
resolveChildren: (parent?: ICompositeTreeNode) => Promise<ITreeNodeOrCompositeTreeNode [] | null > ;
|
||||
resolveChildren: (parent?: ICompositeTreeNode) => Promise<ITreeNodeOrCompositeTreeNode[] | null>;
|
||||
// 节点排序函数
|
||||
sortComparator?: (a: ITreeNodeOrCompositeTreeNode, b: ITreeNodeOrCompositeTreeNode) => number;
|
||||
// 根节点
|
||||
|
|
|
@ -114,8 +114,16 @@ export interface ITreeWatcher {
|
|||
|
||||
// 事件分发
|
||||
|
||||
notifyWillChangeParent(target: ITreeNodeOrCompositeTreeNode, prevParent: ICompositeTreeNode, newParent: ICompositeTreeNode);
|
||||
notifyDidChangeParent(target: ITreeNodeOrCompositeTreeNode, prevParent: ICompositeTreeNode, newParent: ICompositeTreeNode);
|
||||
notifyWillChangeParent(
|
||||
target: ITreeNodeOrCompositeTreeNode,
|
||||
prevParent: ICompositeTreeNode,
|
||||
newParent: ICompositeTreeNode,
|
||||
);
|
||||
notifyDidChangeParent(
|
||||
target: ITreeNodeOrCompositeTreeNode,
|
||||
prevParent: ICompositeTreeNode,
|
||||
newParent: ICompositeTreeNode,
|
||||
);
|
||||
|
||||
notifyWillDispose(target: ITreeNodeOrCompositeTreeNode);
|
||||
notifyDidDispose(target: ITreeNodeOrCompositeTreeNode);
|
||||
|
|
|
@ -14,7 +14,15 @@ export interface ICustomScrollbarProps {
|
|||
onReachBottom?: any;
|
||||
}
|
||||
|
||||
export const Scrollbars = ({ onScroll, onUpdate, forwardedRef, style, children, className, onReachBottom }: ICustomScrollbarProps) => {
|
||||
export const Scrollbars = ({
|
||||
onScroll,
|
||||
onUpdate,
|
||||
forwardedRef,
|
||||
style,
|
||||
children,
|
||||
className,
|
||||
onReachBottom,
|
||||
}: ICustomScrollbarProps) => {
|
||||
const refSetter = React.useCallback((scrollbarsRef) => {
|
||||
if (scrollbarsRef) {
|
||||
forwardedRef && forwardedRef(scrollbarsRef.view);
|
||||
|
@ -25,23 +33,26 @@ export const Scrollbars = ({ onScroll, onUpdate, forwardedRef, style, children,
|
|||
|
||||
let shadowTopRef: HTMLDivElement | null;
|
||||
|
||||
const handleReachBottom = React.useCallback(throttle((values) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = values;
|
||||
const handleReachBottom = React.useCallback(
|
||||
throttle((values) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = values;
|
||||
|
||||
if (scrollHeight === 0 && clientHeight === 0) {
|
||||
return;
|
||||
}
|
||||
if (scrollHeight === 0 && clientHeight === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pad = 100;
|
||||
const t = ((scrollTop + pad) / (scrollHeight - clientHeight));
|
||||
if (t > 1) {
|
||||
onReachBottom && onReachBottom();
|
||||
}
|
||||
}, 100), [onReachBottom]);
|
||||
const pad = 100;
|
||||
const t = (scrollTop + pad) / (scrollHeight - clientHeight);
|
||||
if (t > 1) {
|
||||
onReachBottom && onReachBottom();
|
||||
}
|
||||
}, 100),
|
||||
[onReachBottom],
|
||||
);
|
||||
|
||||
const handleUpdate = (values) => {
|
||||
const { scrollTop } = values;
|
||||
const shadowTopOpacity = 1 / 20 * Math.min(scrollTop, 20);
|
||||
const shadowTopOpacity = (1 / 20) * Math.min(scrollTop, 20);
|
||||
if (shadowTopRef) {
|
||||
shadowTopRef.style.opacity = String(shadowTopOpacity);
|
||||
}
|
||||
|
@ -53,25 +64,22 @@ export const Scrollbars = ({ onScroll, onUpdate, forwardedRef, style, children,
|
|||
return (
|
||||
<CustomScrollbars
|
||||
ref={refSetter}
|
||||
style={{...style, overflow: 'hidden'}}
|
||||
style={{ ...style, overflow: 'hidden' }}
|
||||
className={cls(className, 'kt-scrollbar')}
|
||||
onUpdate={handleUpdate}
|
||||
onScroll={onScroll}
|
||||
renderThumbVertical={({ style, ...props }) =>
|
||||
<div {...props} className={'scrollbar-thumb-vertical'}/>
|
||||
}
|
||||
renderThumbHorizontal={({ style, ...props }) =>
|
||||
<div {...props} className={'scrollbar-thumb-horizontal'}/>
|
||||
}
|
||||
>
|
||||
renderThumbVertical={({ style, ...props }) => <div {...props} className={'scrollbar-thumb-vertical'} />}
|
||||
renderThumbHorizontal={({ style, ...props }) => <div {...props} className={'scrollbar-thumb-horizontal'} />}
|
||||
>
|
||||
<div
|
||||
ref={(ref) => { shadowTopRef = ref; }}
|
||||
className={'scrollbar-decoration'}/>
|
||||
ref={(ref) => {
|
||||
shadowTopRef = ref;
|
||||
}}
|
||||
className={'scrollbar-decoration'}
|
||||
/>
|
||||
{children}
|
||||
</CustomScrollbars>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrollbarsVirtualList = React.forwardRef((props, ref) => (
|
||||
<Scrollbars {...props} forwardedRef={ref} />
|
||||
));
|
||||
export const ScrollbarsVirtualList = React.forwardRef((props, ref) => <Scrollbars {...props} forwardedRef={ref} />);
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
@kt-scrollbar-width: 10px;
|
||||
|
||||
.kt-scrollbar {
|
||||
|
||||
.scrollbar-thumb-vertical,
|
||||
.scrollbar-thumb-horizontal {
|
||||
opacity: 0;
|
||||
transition: opacity .5s ease;
|
||||
transition: opacity 0.5s ease;
|
||||
background: var(--scrollbarSlider-background);
|
||||
width: @kt-scrollbar-width;
|
||||
height: @kt-scrollbar-height;
|
||||
|
@ -24,7 +23,6 @@
|
|||
|
||||
// 设置滚动条样式
|
||||
&:hover {
|
||||
|
||||
.scrollbar-thumb-vertical,
|
||||
.scrollbar-thumb-horizontal {
|
||||
opacity: 1;
|
||||
|
@ -38,9 +36,6 @@
|
|||
width: 100%;
|
||||
height: 6px;
|
||||
z-index: 999;
|
||||
box-shadow: var(--scrollbar-shadow)0 6px 6px -6px inset;
|
||||
box-shadow: var(--scrollbar-shadow) 0 6px 6px -6px inset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ export interface ISelectProps<T = string> {
|
|||
className?: string;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
loading?: boolean;
|
||||
options?: Array<React.ReactNode > | Array <IDataOption<T>> | IDataOptionGroup<T>[];
|
||||
options?: Array<React.ReactNode> | Array<IDataOption<T>> | IDataOptionGroup<T>[];
|
||||
value?: T;
|
||||
disabled?: boolean;
|
||||
onChange?: (value: T) => void;
|
||||
|
@ -28,8 +28,8 @@ export interface ISelectProps<T = string> {
|
|||
[prop: string]: any;
|
||||
optionStyle?: any;
|
||||
equals?: (v1: T | undefined, v2: T | undefined) => boolean;
|
||||
optionRenderer?: React.FC<{ data: IDataOption<T>, isCurrent: boolean }>;
|
||||
groupTitleRenderer?: React.FC<{ group: IDataOptionGroup<T>, index: number }>;
|
||||
optionRenderer?: React.FC<{ data: IDataOption<T>; isCurrent: boolean }>;
|
||||
groupTitleRenderer?: React.FC<{ group: IDataOptionGroup<T>; index: number }>;
|
||||
headerComponent?: React.FC<any> | React.ComponentClass;
|
||||
footerComponent?: React.FC<any> | React.ComponentClass;
|
||||
/**
|
||||
|
@ -43,7 +43,7 @@ export interface ISelectProps<T = string> {
|
|||
/**
|
||||
* 搜索时,根据输入筛选, 如果showSearch为true,则默认使用 label 判断
|
||||
*/
|
||||
filterOption?: (input: string, options: IDataOption<T>, group?: IDataOptionGroup<T> ) => boolean;
|
||||
filterOption?: (input: string, options: IDataOption<T>, group?: IDataOptionGroup<T>) => boolean;
|
||||
/**
|
||||
* 列表为空时的展示组件
|
||||
*/
|
||||
|
@ -51,7 +51,7 @@ export interface ISelectProps<T = string> {
|
|||
/**
|
||||
* 渲染选中项
|
||||
*/
|
||||
selectedRenderer?: React.FC<{data: IDataOption<T>}> | React.ComponentClass<{data: IDataOption<T>}>;
|
||||
selectedRenderer?: React.FC<{ data: IDataOption<T> }> | React.ComponentClass<{ data: IDataOption<T> }>;
|
||||
|
||||
/**
|
||||
* 在显示可选项之前的操作
|
||||
|
@ -73,27 +73,28 @@ export interface ISelectProps<T = string> {
|
|||
dropdownRenderType?: 'fixed' | 'absolute';
|
||||
}
|
||||
|
||||
export const Option: React.FC<React.PropsWithChildren<{
|
||||
value: string | number | string[];
|
||||
children?: any;
|
||||
className?: string;
|
||||
onClick?: (value: string | number | string[]) => void;
|
||||
optionLabelProp?: string;
|
||||
disabled?: boolean;
|
||||
label?: string | undefined;
|
||||
style?: any
|
||||
}>> = ({
|
||||
value,
|
||||
children,
|
||||
disabled,
|
||||
onClick,
|
||||
className,
|
||||
...otherProps
|
||||
}) => (
|
||||
<span {...otherProps} className={classNames(className, { 'kt-option-disabled': disabled })} onClick={() => onClick && !disabled && onClick(value)}>{children}</span>
|
||||
export const Option: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
value: string | number | string[];
|
||||
children?: any;
|
||||
className?: string;
|
||||
onClick?: (value: string | number | string[]) => void;
|
||||
optionLabelProp?: string;
|
||||
disabled?: boolean;
|
||||
label?: string | undefined;
|
||||
style?: any;
|
||||
}>
|
||||
> = ({ value, children, disabled, onClick, className, ...otherProps }) => (
|
||||
<span
|
||||
{...otherProps}
|
||||
className={classNames(className, { 'kt-option-disabled': disabled })}
|
||||
onClick={() => onClick && !disabled && onClick(value)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
function noop(...args: any) { }
|
||||
function noop(...args: any) {}
|
||||
|
||||
function getValueWithProps<P extends { value: any }>(element: React.ReactElement<P>, key?: string) {
|
||||
if (key) {
|
||||
|
@ -121,8 +122,14 @@ interface MaybeOption {
|
|||
* @deprecated
|
||||
* 从react node 上获取 props 来比较有点反模式,不符合数据和视图解耦的思想
|
||||
*/
|
||||
function getLabelWithChildrenProps<T = string>(value: T | undefined, children: React.ReactNode[] | React.ReactNode, equals: (v1, v2) => boolean = (v1, v2) => v1 === v2) {
|
||||
const nodes = React.Children.toArray(children).filter((v) => React.isValidElement<MaybeOption>(v)) as React.ReactElement[];
|
||||
function getLabelWithChildrenProps<T = string>(
|
||||
value: T | undefined,
|
||||
children: React.ReactNode[] | React.ReactNode,
|
||||
equals: (v1, v2) => boolean = (v1, v2) => v1 === v2,
|
||||
) {
|
||||
const nodes = React.Children.toArray(children).filter((v) =>
|
||||
React.isValidElement<MaybeOption>(v),
|
||||
) as React.ReactElement[];
|
||||
|
||||
const currentOption: React.ReactElement<MaybeOption> | null | undefined = nodes.find((node) => {
|
||||
if (node.props) {
|
||||
|
@ -132,10 +139,14 @@ function getLabelWithChildrenProps<T = string>(value: T | undefined, children: R
|
|||
}
|
||||
return null;
|
||||
});
|
||||
return currentOption ? (currentOption.props?.label || currentOption.props?.value) : nodes[0].props?.label || nodes[0].props?.value;
|
||||
return currentOption
|
||||
? currentOption.props?.label || currentOption.props?.value
|
||||
: nodes[0].props?.label || nodes[0].props?.value;
|
||||
}
|
||||
|
||||
export function isDataOptions<T = any>(options: Array<React.ReactNode | { label: string, value: T}> | undefined): options is Array<{ label: string, value: T, iconClass?: string}> {
|
||||
export function isDataOptions<T = any>(
|
||||
options: Array<React.ReactNode | { label: string; value: T }> | undefined,
|
||||
): options is Array<{ label: string; value: T; iconClass?: string }> {
|
||||
if (!options) {
|
||||
return false;
|
||||
}
|
||||
|
@ -145,7 +156,9 @@ export function isDataOptions<T = any>(options: Array<React.ReactNode | { label:
|
|||
return isDataOption(options[0]);
|
||||
}
|
||||
|
||||
export function isDataOptionGroups<T = any>(options: Array<React.ReactNode > | Array <IDataOption<T>> | IDataOptionGroup<T>[] | undefined): options is IDataOptionGroup<T>[] {
|
||||
export function isDataOptionGroups<T = any>(
|
||||
options: Array<React.ReactNode> | Array<IDataOption<T>> | IDataOptionGroup<T>[] | undefined,
|
||||
): options is IDataOptionGroup<T>[] {
|
||||
if (!options) {
|
||||
return false;
|
||||
}
|
||||
|
@ -155,7 +168,9 @@ export function isDataOptionGroups<T = any>(options: Array<React.ReactNode > | A
|
|||
return isDataOptionGroup(options[0]);
|
||||
}
|
||||
|
||||
function isDataOption<T = any>(option: React.ReactNode | { label: string, value: T}): option is { label: string, value: T, iconClass?: string} {
|
||||
function isDataOption<T = any>(
|
||||
option: React.ReactNode | { label: string; value: T },
|
||||
): option is { label: string; value: T; iconClass?: string } {
|
||||
return (option as any).value !== undefined;
|
||||
}
|
||||
|
||||
|
@ -163,24 +178,23 @@ function isDataOptionGroup<T = any>(option: any): option is IDataOptionGroup<T>
|
|||
return (option as any).groupName !== undefined && isDataOptions((option as any).options);
|
||||
}
|
||||
|
||||
function defaultOptionRenderer<T>(v: { data: IDataOption<T>, isCurrent: boolean}) {
|
||||
return <React.Fragment>
|
||||
{v.data.iconClass ?
|
||||
<div className={classNames(v.data.iconClass, 'kt-select-option-icon')}></div>
|
||||
: undefined
|
||||
}
|
||||
{v.data.label}
|
||||
</React.Fragment>;
|
||||
function defaultOptionRenderer<T>(v: { data: IDataOption<T>; isCurrent: boolean }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{v.data.iconClass ? <div className={classNames(v.data.iconClass, 'kt-select-option-icon')}></div> : undefined}
|
||||
{v.data.label}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function defaultGroupTitleRenderer<T>({group, index}: {group: IDataOptionGroup<T>, index: number} ) {
|
||||
return <div key={'header_' + index} className={'kt-select-group-header'}>
|
||||
{group.iconClass ?
|
||||
<div className={classNames(group.iconClass, 'kt-select-option-icon')}></div>
|
||||
: undefined
|
||||
}<div>{group.groupName}</div>
|
||||
</div>;
|
||||
}
|
||||
function defaultGroupTitleRenderer<T>({ group, index }: { group: IDataOptionGroup<T>; index: number }) {
|
||||
return (
|
||||
<div key={'header_' + index} className={'kt-select-group-header'}>
|
||||
{group.iconClass ? <div className={classNames(group.iconClass, 'kt-select-option-icon')}></div> : undefined}
|
||||
<div>{group.groupName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function defaultFilterOption<T>(input: string, option: IDataOption<T>) {
|
||||
let strToSearch: any = option.label;
|
||||
|
@ -231,7 +245,7 @@ export function Select<T = string>({
|
|||
const overlayRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
function toggleOpen() {
|
||||
const target: boolean = !open;
|
||||
const target = !open;
|
||||
if (target) {
|
||||
if (onBeforeShowOptions && onBeforeShowOptions()) {
|
||||
return;
|
||||
|
@ -257,17 +271,33 @@ export function Select<T = string>({
|
|||
|
||||
function Wrapper(node: React.ReactNode, index: number) {
|
||||
if (typeof node === 'string' || typeof node === 'number') {
|
||||
node = <Option value={node} label={String(node)} key={`${node}_${index}`}>{node}</Option>;
|
||||
node = (
|
||||
<Option value={node} label={String(node)} key={`${node}_${index}`}>
|
||||
{node}
|
||||
</Option>
|
||||
);
|
||||
}
|
||||
const disabled = (node as React.ReactElement).props?.disabled || false;
|
||||
return <div key={`${(node as React.ReactElement).props.value}_${index}`} className={classNames({
|
||||
['kt-select-option-select']: value === (node as React.ReactElement).props.value,
|
||||
})} onClick={disabled ? noop : () => {
|
||||
setOpen(false);
|
||||
if (onChange) {
|
||||
onChange(getValueWithProps((node as React.ReactElement), optionLabelProp));
|
||||
}
|
||||
}}>{node}</div>;
|
||||
return (
|
||||
<div
|
||||
key={`${(node as React.ReactElement).props.value}_${index}`}
|
||||
className={classNames({
|
||||
['kt-select-option-select']: value === (node as React.ReactElement).props.value,
|
||||
})}
|
||||
onClick={
|
||||
disabled
|
||||
? noop
|
||||
: () => {
|
||||
setOpen(false);
|
||||
if (onChange) {
|
||||
onChange(getValueWithProps(node as React.ReactElement, optionLabelProp));
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
{node}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -282,10 +312,11 @@ export function Select<T = string>({
|
|||
}
|
||||
// 防止戳出下方屏幕
|
||||
const toBottom = window.innerHeight - boxRect.bottom;
|
||||
if (!maxHeight || (toBottom < parseInt(maxHeight, 10))) {
|
||||
if (!maxHeight || toBottom < parseInt(maxHeight, 10)) {
|
||||
overlayRef.current.style.maxHeight = `${toBottom}px`;
|
||||
}
|
||||
overlayRef.current.style.top = dropdownRenderType === 'fixed' ? `${boxRect.top + boxRect.height}px` : `${boxRect.height}px`;
|
||||
overlayRef.current.style.top =
|
||||
dropdownRenderType === 'fixed' ? `${boxRect.top + boxRect.height}px` : `${boxRect.height}px`;
|
||||
overlayRef.current.style.position = dropdownRenderType === 'fixed' ? 'fixed' : 'absolute';
|
||||
}
|
||||
if (open) {
|
||||
|
@ -354,7 +385,7 @@ export function Select<T = string>({
|
|||
} else if (options && isDataOptionGroups(options)) {
|
||||
const result: Array<IDataOptionGroup<T>> = [];
|
||||
for (const group of options) {
|
||||
const filteredGroup: IDataOptionGroup<T> = {
|
||||
const filteredGroup: IDataOptionGroup<T> = {
|
||||
iconClass: group.iconClass,
|
||||
groupName: group.groupName,
|
||||
options: group.options.filter((o) => filterOption(searchInput, o, group)),
|
||||
|
@ -371,68 +402,75 @@ export function Select<T = string>({
|
|||
const renderSelected = () => {
|
||||
const selected = getSelectedValue();
|
||||
const SC = selectedRenderer;
|
||||
return <React.Fragment >
|
||||
{
|
||||
SC ? <SC data={selected}/> :
|
||||
<React.Fragment>
|
||||
{selected.iconClass ? <span className={classNames(selected.iconClass, 'kt-select-option-icon')}></span> : undefined}
|
||||
<span className={'kt-select-option'}>{selected.label}</span>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Icon iconClass={getKaitianIcon('down')} />
|
||||
</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{SC ? (
|
||||
<SC data={selected} />
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{selected.iconClass ? (
|
||||
<span className={classNames(selected.iconClass, 'kt-select-option-icon')}></span>
|
||||
) : undefined}
|
||||
<span className={'kt-select-option'}>{selected.label}</span>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Icon iconClass={getKaitianIcon('down')} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSearch = () => {
|
||||
return <input
|
||||
className={classNames('kt-select-search')}
|
||||
onChange={(e) => {setSearchInput(e.target.value); }}
|
||||
value={searchInput}
|
||||
autoFocus
|
||||
placeholder={searchPlaceholder || ''}
|
||||
/>;
|
||||
};
|
||||
const renderSearch = () => (
|
||||
<input
|
||||
className={classNames('kt-select-search')}
|
||||
onChange={(e) => {
|
||||
setSearchInput(e.target.value);
|
||||
}}
|
||||
value={searchInput}
|
||||
autoFocus
|
||||
placeholder={searchPlaceholder || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
return (<div className={classNames('kt-select-container', className)} ref={selectRef}>
|
||||
<p className={selectClasses} onClick={toggleOpen} style={style}>
|
||||
{ showSearch && open ? renderSearch() : renderSelected() }
|
||||
</p>
|
||||
return (
|
||||
<div className={classNames('kt-select-container', className)} ref={selectRef}>
|
||||
<p className={selectClasses} onClick={toggleOpen} style={style}>
|
||||
{showSearch && open ? renderSearch() : renderSelected()}
|
||||
</p>
|
||||
|
||||
{
|
||||
open && (
|
||||
(isDataOptions(options) || isDataOptionGroups(options)) ?
|
||||
<SelectOptionsList
|
||||
optionRenderer={optionRenderer}
|
||||
options={options}
|
||||
equals={equals}
|
||||
optionStyle={optionStyle}
|
||||
currentValue={value}
|
||||
size={size}
|
||||
onSelect={(value: T) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
groupTitleRenderer={groupTitleRenderer}
|
||||
className={optionsContainerClasses}
|
||||
style={{ maxHeight: `${maxHeight}px` }}
|
||||
ref={overlayRef}
|
||||
footerComponent={footerComponent}
|
||||
headerComponent={headerComponent}
|
||||
emptyComponent={emptyComponent}
|
||||
/> :
|
||||
// FIXME: to be deprecated
|
||||
// 下面这种使用 children 的方式不够标准化,待废弃
|
||||
<div className={optionsContainerClasses} style={{ maxHeight: `${maxHeight}px` }} ref={overlayRef}>
|
||||
{options && (options as React.ReactNode[]).map((v, i) => {
|
||||
return Wrapper(v, i);
|
||||
})}
|
||||
{children && flatChildren(children, Wrapper)}
|
||||
<div className='kt-select-overlay' onClick={toggleOpen}></div>
|
||||
</div>
|
||||
)}
|
||||
</div>);
|
||||
{open &&
|
||||
(isDataOptions(options) || isDataOptionGroups(options) ? (
|
||||
<SelectOptionsList
|
||||
optionRenderer={optionRenderer}
|
||||
options={options}
|
||||
equals={equals}
|
||||
optionStyle={optionStyle}
|
||||
currentValue={value}
|
||||
size={size}
|
||||
onSelect={(value: T) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
groupTitleRenderer={groupTitleRenderer}
|
||||
className={optionsContainerClasses}
|
||||
style={{ maxHeight: `${maxHeight}px` }}
|
||||
ref={overlayRef}
|
||||
footerComponent={footerComponent}
|
||||
headerComponent={headerComponent}
|
||||
emptyComponent={emptyComponent}
|
||||
/>
|
||||
) : (
|
||||
// FIXME: to be deprecated
|
||||
// 下面这种使用 children 的方式不够标准化,待废弃
|
||||
<div className={optionsContainerClasses} style={{ maxHeight: `${maxHeight}px` }} ref={overlayRef}>
|
||||
{options && (options as React.ReactNode[]).map((v, i) => Wrapper(v, i))}
|
||||
{children && flatChildren(children, Wrapper)}
|
||||
<div className='kt-select-overlay' onClick={toggleOpen}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ISelectOptionsListProps<T = string> {
|
||||
|
@ -443,8 +481,8 @@ export interface ISelectOptionsListProps<T = string> {
|
|||
onSelect: (value: T) => void;
|
||||
optionStyle?: any;
|
||||
equals?: (v1: T | undefined, v2: T | undefined) => boolean;
|
||||
optionRenderer?: React.FC<{ data: IDataOption<T>, isCurrent: boolean }>;
|
||||
groupTitleRenderer?: React.FC<{ group: IDataOptionGroup<T>, index: number }>;
|
||||
optionRenderer?: React.FC<{ data: IDataOption<T>; isCurrent: boolean }>;
|
||||
groupTitleRenderer?: React.FC<{ group: IDataOptionGroup<T>; index: number }>;
|
||||
style?: any;
|
||||
renderCheck?: boolean;
|
||||
headerComponent?: React.FC<any> | React.ComponentClass;
|
||||
|
@ -452,7 +490,7 @@ export interface ISelectOptionsListProps<T = string> {
|
|||
emptyComponent?: React.FC<any> | React.ComponentClass;
|
||||
}
|
||||
|
||||
export const SelectOptionsList = React.forwardRef(<T, >(props: ISelectOptionsListProps<T>, ref) => {
|
||||
export const SelectOptionsList = React.forwardRef(<T,>(props: ISelectOptionsListProps<T>, ref) => {
|
||||
const {
|
||||
options,
|
||||
optionRenderer: OPC = defaultOptionRenderer,
|
||||
|
@ -470,35 +508,53 @@ export const SelectOptionsList = React.forwardRef(<T, >(props: ISelectOptionsLis
|
|||
emptyComponent: EC,
|
||||
} = props;
|
||||
|
||||
const optionsContainerClasses = classNames('kt-select-options', {
|
||||
[`kt-select-options-${size}`]: true,
|
||||
}, className);
|
||||
const optionsContainerClasses = classNames(
|
||||
'kt-select-options',
|
||||
{
|
||||
[`kt-select-options-${size}`]: true,
|
||||
},
|
||||
className,
|
||||
);
|
||||
|
||||
function renderWithGroup(groups: IDataOptionGroup<T>[]) {
|
||||
return groups.map((group, index) => {
|
||||
const header = <GT group={group} index={index} />;
|
||||
return <React.Fragment key={'group_' + index}>
|
||||
{header}
|
||||
{renderWithoutGroup(group.options)}
|
||||
</React.Fragment>;
|
||||
return (
|
||||
<React.Fragment key={'group_' + index}>
|
||||
{header}
|
||||
{renderWithoutGroup(group.options)}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderWithoutGroup(options: IDataOption<T>[]) {
|
||||
return options && options.map((v, index) => {
|
||||
const isCurrent = equals(currentValue, v.value);
|
||||
return <Option value={index} key={index} className={classNames({
|
||||
['kt-select-option-select']: isCurrent,
|
||||
['kt-select-option-default']: true,
|
||||
['kt-option-with-check']: renderCheck,
|
||||
})} onClick={() => onSelect(v.value)} style={optionStyle}>
|
||||
{
|
||||
renderCheck && equals(currentValue, v.value) ? <div className={'kt-option-check'}><Icon icon={'check'} /></div> : undefined
|
||||
}
|
||||
<OPC data={v} isCurrent={isCurrent} />
|
||||
</Option>;
|
||||
|
||||
});
|
||||
return (
|
||||
options &&
|
||||
options.map((v, index) => {
|
||||
const isCurrent = equals(currentValue, v.value);
|
||||
return (
|
||||
<Option
|
||||
value={index}
|
||||
key={index}
|
||||
className={classNames({
|
||||
['kt-select-option-select']: isCurrent,
|
||||
['kt-select-option-default']: true,
|
||||
['kt-option-with-check']: renderCheck,
|
||||
})}
|
||||
onClick={() => onSelect(v.value)}
|
||||
style={optionStyle}
|
||||
>
|
||||
{renderCheck && equals(currentValue, v.value) ? (
|
||||
<div className={'kt-option-check'}>
|
||||
<Icon icon={'check'} />
|
||||
</div>
|
||||
) : undefined}
|
||||
<OPC data={v} isCurrent={isCurrent} />
|
||||
</Option>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
let isEmpty: boolean;
|
||||
if (isDataOptionGroups(options)) {
|
||||
|
@ -507,18 +563,24 @@ export const SelectOptionsList = React.forwardRef(<T, >(props: ISelectOptionsLis
|
|||
isEmpty = options.length === 0;
|
||||
}
|
||||
|
||||
return <div className={optionsContainerClasses} style={style} ref={ref} onClick={
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}>
|
||||
{ HC ? <HC /> : null }
|
||||
{
|
||||
isEmpty && EC ? <EC /> :
|
||||
((isDataOptionGroups(options)) ? renderWithGroup(options) : renderWithoutGroup(options)) || (EC && <EC />)
|
||||
}
|
||||
{ FC ? <FC /> : null }
|
||||
</div>;
|
||||
return (
|
||||
<div
|
||||
className={optionsContainerClasses}
|
||||
style={style}
|
||||
ref={ref}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{HC ? <HC /> : null}
|
||||
{isEmpty && EC ? (
|
||||
<EC />
|
||||
) : (
|
||||
(isDataOptionGroups(options) ? renderWithGroup(options) : renderWithoutGroup(options)) || (EC && <EC />)
|
||||
)}
|
||||
{FC ? <FC /> : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.kt-select-value-active, &:hover {
|
||||
.kt-select-value-active,
|
||||
&:hover {
|
||||
box-sizing: border-box;
|
||||
border-color: var(--kt-selectOption-activeBorder);
|
||||
}
|
||||
|
@ -71,24 +72,26 @@
|
|||
border-color: var(--kt-selectOption-activeBorder);
|
||||
}
|
||||
|
||||
.kt-select-value-large, .kt-select-value-large > span {
|
||||
.kt-select-value-large,
|
||||
.kt-select-value-large > span {
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.kt-select-value-default, .kt-select-value-default > span {
|
||||
.kt-select-value-default,
|
||||
.kt-select-value-default > span {
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.kt-select-value-small, .kt-select-value-small > span {
|
||||
.kt-select-value-small,
|
||||
.kt-select-value-small > span {
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.kt-select-option-icon {
|
||||
|
@ -111,13 +114,12 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
.kt-select-option-icon {
|
||||
margin-right:5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kt-select-option-select {
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
@ -159,7 +161,8 @@
|
|||
&.kt-select-options-large {
|
||||
padding: 4px 0px;
|
||||
|
||||
& > span, & > div > span {
|
||||
& > span,
|
||||
& > div > span {
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
|
@ -173,7 +176,8 @@
|
|||
&.kt-select-options-default {
|
||||
padding: 3px 0px;
|
||||
|
||||
& > span, & > div > span {
|
||||
& > span,
|
||||
& > div > span {
|
||||
height: 26px;
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
|
@ -187,7 +191,8 @@
|
|||
&.kt-select-options-small {
|
||||
padding: 3px 0px;
|
||||
|
||||
& > span, & > div > span {
|
||||
& > span,
|
||||
& > div > span {
|
||||
height: 26px;
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
|
@ -203,14 +208,14 @@
|
|||
}
|
||||
|
||||
.kt-select-group-header {
|
||||
padding-left:5px;
|
||||
padding-left: 5px;
|
||||
color: var(--kt-selectDropdown-teamForeground);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.kt-option-check {
|
||||
position: absolute;
|
||||
left:0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -221,4 +226,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -518,7 +518,6 @@ mark {
|
|||
.clearfix();
|
||||
}
|
||||
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
.iconfont-mixin();
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "variable.less";
|
||||
@import 'variable.less';
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
|
|
|
@ -15,12 +15,11 @@
|
|||
@white: #fff;
|
||||
@black: #000;
|
||||
|
||||
@primary-1 : #e6f7ff;
|
||||
@primary-1: #e6f7ff;
|
||||
@yellow-1: #f6ffed;
|
||||
|
||||
@font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
@font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
|
||||
'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
@code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
@text-color-inverse: @white;
|
||||
@text-selection-bg: @primary-color;
|
||||
|
|
|
@ -21,26 +21,30 @@ export const Tabs = (props: ITabsProps) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div {...restProps} style={style} className={clx('kt-tabs', className, { ['kt-tabs-mini']: mini } )}>
|
||||
{
|
||||
tabs.map((tabContent, i) => {
|
||||
const selectedClassName = i === value ? 'kt-tab-selected' : '';
|
||||
if (typeof tabContent === 'string') {
|
||||
return <div
|
||||
<div {...restProps} style={style} className={clx('kt-tabs', className, { ['kt-tabs-mini']: mini })}>
|
||||
{tabs.map((tabContent, i) => {
|
||||
const selectedClassName = i === value ? 'kt-tab-selected' : '';
|
||||
if (typeof tabContent === 'string') {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={clx('kt-tab', selectedClassName, { ['kt-mini-tab']: mini })}
|
||||
onClick={onClick.bind(null, i)}>
|
||||
onClick={onClick.bind(null, i)}
|
||||
>
|
||||
{tabContent}
|
||||
</div>;
|
||||
}
|
||||
return <div
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={clx('kt-custom-tab', selectedClassName, { ['kt-mini-tab']: mini })}
|
||||
onClick={onClick.bind(null, i)}>
|
||||
onClick={onClick.bind(null, i)}
|
||||
>
|
||||
{tabContent}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,11 +7,7 @@ import './style.less';
|
|||
export const Tooltip: React.FC<{
|
||||
title: string;
|
||||
delay?: number;
|
||||
}> = ({
|
||||
title,
|
||||
children,
|
||||
delay,
|
||||
}) => {
|
||||
}> = ({ title, children, delay }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const targetRef = useRef<HTMLParagraphElement | null>(null);
|
||||
const tooltipRef = useRef<HTMLSpanElement | null>(null);
|
||||
|
@ -33,16 +29,16 @@ export const Tooltip: React.FC<{
|
|||
|
||||
if (y < tooltipRect.height) {
|
||||
arrowRef.current.className += ' kt-tooltip-reverse-arrow';
|
||||
tooltipRef.current.style.top = `${y + height + (tooltipRect.height / 2)}px`;
|
||||
tooltipRef.current.style.top = `${y + height + tooltipRect.height / 2}px`;
|
||||
} else {
|
||||
tooltipRef.current.style.top = `${y - (height / 2) - (tooltipRect.height / 2)}px`;
|
||||
tooltipRef.current.style.top = `${y - height / 2 - tooltipRect.height / 2}px`;
|
||||
}
|
||||
|
||||
if (x + tooltipRect.width >= document.body.offsetWidth) {
|
||||
arrowRef.current.style.left = `${x + tooltipRect.width - document.body.offsetWidth + (width / 2)}px`;
|
||||
arrowRef.current.style.left = `${x + tooltipRect.width - document.body.offsetWidth + width / 2}px`;
|
||||
tooltipRef.current.style.left = `${document.body.offsetWidth - tooltipRect.width}px`;
|
||||
} else {
|
||||
tooltipRef.current.style.left = `${x + (width / 2) - (tooltipRect.width / 2)}px`;
|
||||
tooltipRef.current.style.left = `${x + width / 2 - tooltipRect.width / 2}px`;
|
||||
}
|
||||
}
|
||||
clearTimeout(timer);
|
||||
|
@ -56,8 +52,15 @@ export const Tooltip: React.FC<{
|
|||
setVisible(false);
|
||||
}
|
||||
|
||||
return (<p ref={targetRef} className={'kt-tooltip-wrapper'} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{children}
|
||||
{visible && <span ref={tooltipRef} className={cxs('kt-tooltip-content')}>{title}<span ref={arrowRef} className={'kt-tooltip-arrow-placeholder'} /></span>}
|
||||
</p>);
|
||||
return (
|
||||
<p ref={targetRef} className={'kt-tooltip-wrapper'} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{children}
|
||||
{visible && (
|
||||
<span ref={tooltipRef} className={cxs('kt-tooltip-content')}>
|
||||
{title}
|
||||
<span ref={arrowRef} className={'kt-tooltip-arrow-placeholder'} />
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue