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:
Artin 2021-12-17 12:52:32 +08:00 committed by GitHub
parent 1b9298145b
commit 8a43372945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1723 changed files with 71435 additions and 52967 deletions

View File

@ -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

View File

@ -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',
},
};

18
.prettierignore Normal file
View File

@ -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

View File

@ -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',
],

View File

@ -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",

View File

@ -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);
});

View File

@ -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);
// 获取对象实例的时候才开始注册事件

View File

@ -7,9 +7,7 @@ describe('test for ', () => {
let injector: MockInjector;
beforeEach(() => {
injector = createNodeInjector([
AddonsModule,
]);
injector = createNodeInjector([AddonsModule]);
});
it('empty module', () => {

View File

@ -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']>) {

View File

@ -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;

View File

@ -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);
});
},
() => {},
);
}
}

View File

@ -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',
});
}
}

View File

@ -27,5 +27,4 @@ export class ClientAddonModule extends BrowserModule {
servicePath: FileDropServicePath,
},
];
}

View File

@ -4,7 +4,6 @@ import { IDialogService } from '@opensumi/ide-overlay';
@Domain(ClientAppContribution)
export class LanguageChangeHintContribution implements ClientAppContribution {
@Autowired(PreferenceService)
preferenceService: PreferenceService;

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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
},
});
}
}

View File

@ -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>;
);
};

View File

@ -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);
});

View File

@ -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',
}],
],
}),
];
}

View File

@ -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

View File

@ -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)}
>&nbsp;{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)}
>
&nbsp;{reaction.count}
</Button>
))}
</div>
);
});

View File

@ -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>
);
});

View File

@ -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,
};
}

View File

@ -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>

View File

@ -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>
);

View File

@ -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>
);
});

View File

@ -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);
}

View File

@ -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),
);
}
}

View File

@ -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
}
}

View File

@ -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
},
});
}
}

View File

@ -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() {

View File

@ -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)',
},
},
},
};
};
},
});

View File

@ -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 {

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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>
);

View File

@ -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>
);
});
);
},
);

View File

@ -58,7 +58,7 @@
@keyframes loadingCircle {
100% {
transform: rotate(360deg)
transform: rotate(360deg);
}
}

View File

@ -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',

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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();

View File

@ -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;

View File

@ -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;
}

View File

@ -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
// -------------------------

View File

@ -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}
/>
);
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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>
);
}

View File

@ -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();

View File

@ -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} />;
}
}

View File

@ -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} />;
}
}

View File

@ -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

View File

@ -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 = {

View File

@ -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 {

View File

@ -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();

View File

@ -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,

View File

@ -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;

View File

@ -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() {

View File

@ -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>
);

View File

@ -40,5 +40,4 @@
color: var(--kt-notificationsCloseIcon-foreground);
}
}
}

View File

@ -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>
);
};

View File

@ -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');
}
}

View File

@ -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>;
}
};

View File

@ -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>
);
}
}

View File

@ -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) {

View File

@ -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>
);
};

View File

@ -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>;

View File

@ -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;
}
}
}

View File

@ -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;
// 每个节点应该拥有自己独立的路径,不存在重复性

View File

@ -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>

View File

@ -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();

View File

@ -130,8 +130,8 @@ export interface IBasicRecycleTreeProps {
*/
onContextMenu?: (event: React.MouseEvent, node?: ITreeNodeOrCompositeTreeNode) => void;
/**
*
*/
*
*/
onTwistierClick?: (event: React.MouseEvent, node: ITreeNodeOrCompositeTreeNode) => void;
/**
* `onContextMenu` `onContextMenu`

View File

@ -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,
})
}
</>
);
};
})}
</>
);
};

View File

@ -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),

View File

@ -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();
}
}
};
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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 });
}
}
}

View File

@ -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);
}
}
}
};
}

View File

@ -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[]);
}
}

View File

@ -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();
}

View File

@ -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> {

View File

@ -2,8 +2,8 @@ export interface ISerializableState {
specVersion: number;
scrollPosition: number;
expandedDirectories: {
atSurface: string[],
buried: string[],
atSurface: string[];
buried: string[];
};
}

View File

@ -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;
// 根节点

View File

@ -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);

View File

@ -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} />);

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 @@
}
}
}

View File

@ -518,7 +518,6 @@ mark {
.clearfix();
}
.@{iconfont-css-prefix} {
.iconfont-mixin();

View File

@ -1,4 +1,4 @@
@import "variable.less";
@import 'variable.less';
.text-ellipsis {
overflow: hidden;

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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