@teambit/command-bar 0.0.566 → 0.0.570
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/command-bar.preview.runtime.tsx +40 -0
- package/command-bar.ui.runtime.tsx +177 -0
- package/model/key-event/index.ts +1 -0
- package/model/key-event/key-event.tsx +9 -0
- package/model/key-event/serialize-keyboard-event.ts +16 -0
- package/package-tar/teambit-command-bar-0.0.570.tgz +0 -0
- package/package.json +12 -27
- package/types/asset.d.ts +29 -0
- package/types/style.d.ts +42 -0
- package/types.tsx +20 -0
- package/ui/autocomplete-input/autocomplete-input.tsx +62 -0
- package/ui/autocomplete-input/index.ts +1 -0
- package/ui/command-bar/command-bar.tsx +66 -0
- package/ui/command-bar/index.ts +1 -0
- package/ui/command-bar-context/command-bar-context.tsx +4 -0
- package/ui/command-bar-context/index.ts +1 -0
- package/ui/command-bar-item/command-bar-item.tsx +30 -0
- package/ui/command-bar-item/index.ts +1 -0
- package/ui/command-searcher/command-searcher.ts +31 -0
- package/ui/command-searcher/index.ts +1 -0
- package/ui/component-searcher/component-searcher.ts +44 -0
- package/ui/component-searcher/index.ts +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PreviewRuntime } from '@teambit/preview';
|
|
2
|
+
import { PubsubAspect, PubsubPreview } from '@teambit/pubsub';
|
|
3
|
+
import { isOpenCommandBarKeybinding } from './keybinding';
|
|
4
|
+
import { CommandBarAspect } from './command-bar.aspect';
|
|
5
|
+
import { KeyEvent } from './model/key-event';
|
|
6
|
+
|
|
7
|
+
export class CommandBarPreview {
|
|
8
|
+
constructor(private pubSub: PubsubPreview) {
|
|
9
|
+
document.addEventListener('keydown', this.handleKeyEvent);
|
|
10
|
+
document.addEventListener('keypress', this.handleKeyEvent);
|
|
11
|
+
document.addEventListener('keyup', this.handleKeyEvent);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
handleKeyEvent = (e: KeyboardEvent) => {
|
|
15
|
+
const { target } = e;
|
|
16
|
+
if (!target || isEditable(target as HTMLElement)) return;
|
|
17
|
+
if (isDenyListed(e)) e.preventDefault();
|
|
18
|
+
|
|
19
|
+
this.pubSub.pub(CommandBarAspect.id, new KeyEvent(e));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static dependencies = [PubsubAspect];
|
|
23
|
+
static runtime = PreviewRuntime;
|
|
24
|
+
static async provider([pubSub]: [PubsubPreview]) {
|
|
25
|
+
const pubsubPreview = new CommandBarPreview(pubSub);
|
|
26
|
+
return pubsubPreview;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const editableTags = ['INPUT', 'SELECT', 'TEXTAREA'];
|
|
31
|
+
function isEditable(element: HTMLElement) {
|
|
32
|
+
return editableTags.includes(element.tagName) || element.isContentEditable;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
CommandBarAspect.addRuntime(CommandBarPreview);
|
|
36
|
+
|
|
37
|
+
// block default browser behavior that would override our keybinding.
|
|
38
|
+
function isDenyListed(e: KeyboardEvent) {
|
|
39
|
+
return isOpenCommandBarKeybinding(e);
|
|
40
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Slot, SlotRegistry } from '@teambit/harmony';
|
|
3
|
+
import Mousetrap from 'mousetrap';
|
|
4
|
+
|
|
5
|
+
import UIAspect, { UIRuntime, UiUI } from '@teambit/ui';
|
|
6
|
+
import { PubsubAspect, PubsubUI } from '@teambit/pubsub';
|
|
7
|
+
import { isBrowser } from '@teambit/ui-foundation.ui.is-browser';
|
|
8
|
+
import { CommandBar } from './ui/command-bar';
|
|
9
|
+
import { CommandSearcher } from './ui/command-searcher';
|
|
10
|
+
import { CommandBarAspect } from './command-bar.aspect';
|
|
11
|
+
import { commandBarCommands } from './command-bar.commands';
|
|
12
|
+
import { SearchProvider, Keybinding, CommandHandler, CommandId } from './types';
|
|
13
|
+
import { DuplicateCommandError } from './duplicate-command-error';
|
|
14
|
+
import { KeyEvent } from './model/key-event';
|
|
15
|
+
import { CommandBarContext } from './ui/command-bar-context';
|
|
16
|
+
import { MousetrapStub } from './mousetrap-stub';
|
|
17
|
+
import { openCommandBarKeybinding } from './keybinding';
|
|
18
|
+
|
|
19
|
+
const RESULT_LIMIT = 5;
|
|
20
|
+
type SearcherSlot = SlotRegistry<SearchProvider>;
|
|
21
|
+
type CommandSlot = SlotRegistry<CommandEntry[]>;
|
|
22
|
+
|
|
23
|
+
export type CommandEntry = {
|
|
24
|
+
id: CommandId;
|
|
25
|
+
handler: CommandHandler;
|
|
26
|
+
keybinding?: Keybinding;
|
|
27
|
+
displayName: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Quick launch actions. Use the `addSearcher` slot to extend the available actions */
|
|
31
|
+
export class CommandBarUI {
|
|
32
|
+
private mousetrap = isBrowser ? new Mousetrap() : new MousetrapStub();
|
|
33
|
+
private commandSearcher = new CommandSearcher([]);
|
|
34
|
+
|
|
35
|
+
/** Opens the command bar */
|
|
36
|
+
open = () => {
|
|
37
|
+
this.setVisibility?.(true);
|
|
38
|
+
return false; // aka prevent default
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Closes the command bar */
|
|
42
|
+
close = () => {
|
|
43
|
+
this.setVisibility?.(false);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Add and autocomplete provider. To support keyboard navigation, each result should have a props `active: boolean`, and `exectue: () => any` */
|
|
47
|
+
addSearcher(commandSearcher: SearchProvider) {
|
|
48
|
+
this.searcherSlot.register(commandSearcher);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* registers a command
|
|
54
|
+
*/
|
|
55
|
+
addCommand(...originalCommands: CommandEntry[]) {
|
|
56
|
+
originalCommands.forEach(({ id: commandId }) => {
|
|
57
|
+
if (this.getCommand(commandId) !== undefined) throw new DuplicateCommandError(commandId);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// commands could mutate later on, clone to ensure immutability 👌
|
|
61
|
+
const commands = originalCommands.map((x) => ({
|
|
62
|
+
id: x.id,
|
|
63
|
+
displayName: x.displayName,
|
|
64
|
+
handler: x.handler,
|
|
65
|
+
keybinding: x.keybinding,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
this.commandSlot.register(commands);
|
|
69
|
+
|
|
70
|
+
commands.forEach((command) => {
|
|
71
|
+
if (command.keybinding) {
|
|
72
|
+
this.addKeybinding(command.keybinding, command.id);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.updateCommandsSearcher();
|
|
77
|
+
|
|
78
|
+
const updaters = commands.map((command) => (next: CommandHandler) => {
|
|
79
|
+
command.handler = next;
|
|
80
|
+
});
|
|
81
|
+
return updaters;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* executes command by name, if exists.
|
|
86
|
+
* @param commandId
|
|
87
|
+
*/
|
|
88
|
+
run(commandId: CommandId) {
|
|
89
|
+
const commandEntry = this.getCommand(commandId);
|
|
90
|
+
if (!commandEntry) return undefined;
|
|
91
|
+
|
|
92
|
+
return commandEntry.handler();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* executes a keyboard shortcut manually
|
|
97
|
+
*/
|
|
98
|
+
trigger = (key: string) => {
|
|
99
|
+
this.mousetrap.trigger(key);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
private search = (term: string, limit: number = RESULT_LIMIT) => {
|
|
103
|
+
const searchers = this.searcherSlot.values();
|
|
104
|
+
|
|
105
|
+
const searcher = searchers.find((x) => x.test(term));
|
|
106
|
+
return searcher?.search(term, limit) || [];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
private getCommand(id: CommandId) {
|
|
110
|
+
const relevantCommands = this.commandSlot
|
|
111
|
+
.values()
|
|
112
|
+
.map((commands) => commands.find((command) => command.id === id))
|
|
113
|
+
.filter((x) => !!x);
|
|
114
|
+
|
|
115
|
+
return relevantCommands.pop();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private updateCommandsSearcher() {
|
|
119
|
+
const commands = this.commandSlot.values().flat();
|
|
120
|
+
this.commandSearcher.update(commands);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private addKeybinding(key: Keybinding, command: CommandId) {
|
|
124
|
+
this.mousetrap.bind(key, this.run.bind(this, command));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private renderContext = ({ children }: { children: ReactNode }) => {
|
|
128
|
+
return <CommandBarContext.Provider value={this}>{children}</CommandBarContext.Provider>;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* internal. Opens and closes the command bar UI.
|
|
133
|
+
*/
|
|
134
|
+
setVisibility?: (visible: boolean) => void;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* generate the ui for command bar
|
|
138
|
+
*/
|
|
139
|
+
getCommandBar = () => {
|
|
140
|
+
return <CommandBar key="CommandBarUI" search={this.search} commander={this} />;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
constructor(private searcherSlot: SearcherSlot, private commandSlot: CommandSlot, pubSub: PubsubUI) {
|
|
144
|
+
this.addSearcher(this.commandSearcher);
|
|
145
|
+
pubSub.sub(CommandBarAspect.id, (e: KeyEvent) => {
|
|
146
|
+
const keyboardEvent = new KeyboardEvent(e.type, e.data);
|
|
147
|
+
document.dispatchEvent(keyboardEvent);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static dependencies = [UIAspect, PubsubAspect];
|
|
152
|
+
static slots = [Slot.withType<SearchProvider>(), Slot.withType<CommandEntry[]>()];
|
|
153
|
+
static runtime = UIRuntime;
|
|
154
|
+
static async provider(
|
|
155
|
+
[uiUi, pubsubUI]: [UiUI, PubsubUI],
|
|
156
|
+
config,
|
|
157
|
+
[searcherSlot, commandSlots]: [SearcherSlot, CommandSlot]
|
|
158
|
+
) {
|
|
159
|
+
const commandBar = new CommandBarUI(searcherSlot, commandSlots, pubsubUI);
|
|
160
|
+
|
|
161
|
+
commandBar.addCommand({
|
|
162
|
+
id: commandBarCommands.open,
|
|
163
|
+
handler: commandBar.open,
|
|
164
|
+
displayName: 'Open command bar',
|
|
165
|
+
keybinding: openCommandBarKeybinding,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
uiUi.registerHudItem(commandBar.getCommandBar());
|
|
169
|
+
uiUi.registerRenderHooks({
|
|
170
|
+
reactContext: commandBar.renderContext,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return commandBar;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
CommandBarAspect.addRuntime(CommandBarUI);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { KeyEvent } from './key-event';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BitBaseEvent } from '@teambit/pubsub';
|
|
2
|
+
import { serializeKeyboardEvent } from './serialize-keyboard-event';
|
|
3
|
+
|
|
4
|
+
export class KeyEvent extends BitBaseEvent<KeyboardEventInit> {
|
|
5
|
+
constructor(event: KeyboardEvent, timestamp = Date.now()) {
|
|
6
|
+
const serialized = serializeKeyboardEvent(event);
|
|
7
|
+
super(event.type, '0.0.1', timestamp, serialized);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import isPrimitive from 'is-primitive';
|
|
2
|
+
|
|
3
|
+
export function serializeKeyboardEvent(e: KeyboardEvent): KeyboardEventInit {
|
|
4
|
+
const serialized = {};
|
|
5
|
+
|
|
6
|
+
// 'for ... in' includes properties from prototype (which we want)
|
|
7
|
+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
8
|
+
for (const key in e) {
|
|
9
|
+
const val = e[key];
|
|
10
|
+
if (isPrimitive(val)) {
|
|
11
|
+
serialized[key] = val;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return serialized;
|
|
16
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teambit/command-bar",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.570",
|
|
4
4
|
"homepage": "https://bit.dev/teambit/explorer/command-bar",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "teambit.explorer",
|
|
8
8
|
"name": "command-bar",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.570"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@teambit/harmony": "0.2.11",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"core-js": "^3.0.0",
|
|
20
20
|
"@teambit/base-ui.surfaces.card": "1.0.1",
|
|
21
21
|
"@teambit/base-ui.text.muted-text": "1.0.1",
|
|
22
|
-
"@teambit/preview": "0.0.
|
|
23
|
-
"@teambit/pubsub": "0.0.
|
|
24
|
-
"@teambit/ui-foundation.ui.is-browser": "0.0.
|
|
25
|
-
"@teambit/ui": "0.0.
|
|
26
|
-
"@teambit/bit-error": "0.0.
|
|
27
|
-
"@teambit/ui-foundation.ui.constants.z-indexes": "0.0.
|
|
28
|
-
"@teambit/ui-foundation.ui.keycap": "0.0.
|
|
29
|
-
"@teambit/component": "0.0.
|
|
22
|
+
"@teambit/preview": "0.0.570",
|
|
23
|
+
"@teambit/pubsub": "0.0.570",
|
|
24
|
+
"@teambit/ui-foundation.ui.is-browser": "0.0.472",
|
|
25
|
+
"@teambit/ui": "0.0.570",
|
|
26
|
+
"@teambit/bit-error": "0.0.381",
|
|
27
|
+
"@teambit/ui-foundation.ui.constants.z-indexes": "0.0.473",
|
|
28
|
+
"@teambit/ui-foundation.ui.keycap": "0.0.472",
|
|
29
|
+
"@teambit/component": "0.0.570"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/mousetrap": "1.6.5",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@types/node": "12.20.4"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@teambit/legacy": "1.0.
|
|
42
|
+
"@teambit/legacy": "1.0.181",
|
|
43
43
|
"react-dom": "^16.8.0 || ^17.0.0",
|
|
44
44
|
"react": "^16.8.0 || ^17.0.0"
|
|
45
45
|
},
|
|
@@ -67,27 +67,12 @@
|
|
|
67
67
|
"react": "-"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@teambit/legacy": "1.0.
|
|
70
|
+
"@teambit/legacy": "1.0.181",
|
|
71
71
|
"react-dom": "^16.8.0 || ^17.0.0",
|
|
72
72
|
"react": "^16.8.0 || ^17.0.0"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
-
"files": [
|
|
77
|
-
"dist",
|
|
78
|
-
"!dist/tsconfig.tsbuildinfo",
|
|
79
|
-
"**/*.md",
|
|
80
|
-
"**/*.mdx",
|
|
81
|
-
"**/*.js",
|
|
82
|
-
"**/*.json",
|
|
83
|
-
"**/*.sass",
|
|
84
|
-
"**/*.scss",
|
|
85
|
-
"**/*.less",
|
|
86
|
-
"**/*.css",
|
|
87
|
-
"**/*.css",
|
|
88
|
-
"**/*.jpeg",
|
|
89
|
-
"**/*.gif"
|
|
90
|
-
],
|
|
91
76
|
"private": false,
|
|
92
77
|
"engines": {
|
|
93
78
|
"node": ">=12.22.0"
|
package/types/asset.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare module '*.png' {
|
|
2
|
+
const value: any;
|
|
3
|
+
export = value;
|
|
4
|
+
}
|
|
5
|
+
declare module '*.svg' {
|
|
6
|
+
import type { FunctionComponent, SVGProps } from 'react';
|
|
7
|
+
|
|
8
|
+
export const ReactComponent: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string }>;
|
|
9
|
+
const src: string;
|
|
10
|
+
export default src;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// @TODO Gilad
|
|
14
|
+
declare module '*.jpg' {
|
|
15
|
+
const value: any;
|
|
16
|
+
export = value;
|
|
17
|
+
}
|
|
18
|
+
declare module '*.jpeg' {
|
|
19
|
+
const value: any;
|
|
20
|
+
export = value;
|
|
21
|
+
}
|
|
22
|
+
declare module '*.gif' {
|
|
23
|
+
const value: any;
|
|
24
|
+
export = value;
|
|
25
|
+
}
|
|
26
|
+
declare module '*.bmp' {
|
|
27
|
+
const value: any;
|
|
28
|
+
export = value;
|
|
29
|
+
}
|
package/types/style.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
declare module '*.module.css' {
|
|
2
|
+
const classes: { readonly [key: string]: string };
|
|
3
|
+
export default classes;
|
|
4
|
+
}
|
|
5
|
+
declare module '*.module.scss' {
|
|
6
|
+
const classes: { readonly [key: string]: string };
|
|
7
|
+
export default classes;
|
|
8
|
+
}
|
|
9
|
+
declare module '*.module.sass' {
|
|
10
|
+
const classes: { readonly [key: string]: string };
|
|
11
|
+
export default classes;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module '*.module.less' {
|
|
15
|
+
const classes: { readonly [key: string]: string };
|
|
16
|
+
export default classes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module '*.less' {
|
|
20
|
+
const classes: { readonly [key: string]: string };
|
|
21
|
+
export default classes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module '*.css' {
|
|
25
|
+
const classes: { readonly [key: string]: string };
|
|
26
|
+
export default classes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare module '*.sass' {
|
|
30
|
+
const classes: { readonly [key: string]: string };
|
|
31
|
+
export default classes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module '*.scss' {
|
|
35
|
+
const classes: { readonly [key: string]: string };
|
|
36
|
+
export default classes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare module '*.mdx' {
|
|
40
|
+
const component: any;
|
|
41
|
+
export default component;
|
|
42
|
+
}
|
package/types.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CommandId = string;
|
|
2
|
+
export type CommandHandler = (...arg: any[]) => any;
|
|
3
|
+
|
|
4
|
+
export type CommanderSearchResult = {
|
|
5
|
+
id: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
handler: CommandHandler;
|
|
8
|
+
icon?: string;
|
|
9
|
+
iconAlt?: string;
|
|
10
|
+
keybinding?: Keybinding;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface SearchProvider {
|
|
14
|
+
/** provide completions for this search term */
|
|
15
|
+
search(term: string, limit: number): CommanderSearchResult[];
|
|
16
|
+
/** determines what terms are handled by this searcher. */
|
|
17
|
+
test(term: string): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Keybinding = string | string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, createRef, InputHTMLAttributes } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import styles from './autocomplete-input.module.scss';
|
|
4
|
+
|
|
5
|
+
type AutoCompleteInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
|
6
|
+
onEscape?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
7
|
+
onDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
8
|
+
onUp?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
9
|
+
onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
10
|
+
focus?: Truthy;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Truthy = boolean | number | string | null | undefined;
|
|
14
|
+
|
|
15
|
+
export function AutoCompleteInput({
|
|
16
|
+
className,
|
|
17
|
+
onEscape,
|
|
18
|
+
onDown,
|
|
19
|
+
onUp,
|
|
20
|
+
onEnter,
|
|
21
|
+
onKeyDown,
|
|
22
|
+
focus,
|
|
23
|
+
...rest
|
|
24
|
+
}: AutoCompleteInputProps) {
|
|
25
|
+
const inputRef = createRef<HTMLInputElement>();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (focus) inputRef.current?.focus();
|
|
29
|
+
}, [focus]);
|
|
30
|
+
|
|
31
|
+
const handleKeyDown = useCallback(
|
|
32
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
33
|
+
onKeyDown?.(e);
|
|
34
|
+
if (e.defaultPrevented) return;
|
|
35
|
+
|
|
36
|
+
const handlers = {
|
|
37
|
+
Escape: () => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
onEscape?.(e);
|
|
40
|
+
},
|
|
41
|
+
ArrowDown: () => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
onDown?.(e);
|
|
44
|
+
},
|
|
45
|
+
ArrowUp: () => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
onUp?.(e);
|
|
48
|
+
},
|
|
49
|
+
Enter: () => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
onEnter?.(e);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (e.key in handlers) handlers[e.key]();
|
|
56
|
+
},
|
|
57
|
+
[onEscape, onDown, onUp, onEnter]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// @ts-ignore (https://github.com/teambit/bit/issues/4908)
|
|
61
|
+
return <input {...rest} ref={inputRef} className={classNames(styles.input, className)} onKeyDown={handleKeyDown} />;
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './autocomplete-input';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Card, CardProps } from '@teambit/base-ui.surfaces.card';
|
|
4
|
+
import type { CommandBarUI } from '../../command-bar.ui.runtime';
|
|
5
|
+
import { CommanderSearchResult } from '../../types';
|
|
6
|
+
import { AutoCompleteInput } from '../autocomplete-input';
|
|
7
|
+
import { CommandBarItem } from '../command-bar-item';
|
|
8
|
+
import styles from './command-bar.module.scss';
|
|
9
|
+
|
|
10
|
+
export type CommandBarProps = {
|
|
11
|
+
search: (term: string, limit?: number) => CommanderSearchResult[];
|
|
12
|
+
commander: CommandBarUI;
|
|
13
|
+
} & CardProps;
|
|
14
|
+
|
|
15
|
+
const MIN_IDX = 0;
|
|
16
|
+
|
|
17
|
+
export function CommandBar({ search, commander, elevation = 'high', className, ...rest }: CommandBarProps) {
|
|
18
|
+
const [term, setTerm] = useState('');
|
|
19
|
+
const options = useMemo(() => search(term), [term, search]);
|
|
20
|
+
const [activeIdx, setActive] = useState(MIN_IDX);
|
|
21
|
+
const increment = useCallback(() => setActive((x) => Math.min(x + 1, options.length - 1)), [options.length]);
|
|
22
|
+
const decrement = useCallback(() => setActive((x) => Math.max(x - 1, MIN_IDX)), []);
|
|
23
|
+
const [visible, setVisibility] = useState(false);
|
|
24
|
+
|
|
25
|
+
commander.setVisibility = setVisibility;
|
|
26
|
+
|
|
27
|
+
const handleEnter = useCallback(() => {
|
|
28
|
+
setVisibility(false);
|
|
29
|
+
options[activeIdx]?.handler();
|
|
30
|
+
}, [options, activeIdx]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => setTerm(''), [visible]);
|
|
33
|
+
useEffect(() => setActive(MIN_IDX), [options]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Card
|
|
37
|
+
{...rest}
|
|
38
|
+
elevation={elevation}
|
|
39
|
+
className={classNames(className, styles.commandBar, visible && styles.visible)}
|
|
40
|
+
>
|
|
41
|
+
<AutoCompleteInput
|
|
42
|
+
value={term}
|
|
43
|
+
focus={visible}
|
|
44
|
+
className={styles.input}
|
|
45
|
+
placeholder="Search anything or type > to only search commands"
|
|
46
|
+
onChange={(e) => setTerm(e.target.value)}
|
|
47
|
+
onDown={increment}
|
|
48
|
+
onUp={decrement}
|
|
49
|
+
onEnter={handleEnter}
|
|
50
|
+
onEscape={() => setVisibility(false)}
|
|
51
|
+
onBlur={() => setVisibility(false)}
|
|
52
|
+
/>
|
|
53
|
+
<div className={styles.results}>
|
|
54
|
+
{options.map((x, idx) => (
|
|
55
|
+
<CommandBarItem
|
|
56
|
+
key={idx} // use index instead of id to avoid duplicate keys
|
|
57
|
+
entry={x}
|
|
58
|
+
active={idx === activeIdx}
|
|
59
|
+
// mouseDown happens before blur, which closes the command bar
|
|
60
|
+
onMouseDown={x.handler}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</Card>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CommandBar, CommandBarProps } from './command-bar';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CommandBarContext } from './command-bar-context';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classnames from 'classnames';
|
|
3
|
+
import { KeySequence } from '@teambit/ui-foundation.ui.keycap';
|
|
4
|
+
import { mutedText } from '@teambit/base-ui.text.muted-text';
|
|
5
|
+
import { CommanderSearchResult } from '../../types';
|
|
6
|
+
import styles from './command-bar-item.module.scss';
|
|
7
|
+
|
|
8
|
+
export type CommandItemProps = {
|
|
9
|
+
active?: boolean;
|
|
10
|
+
entry: CommanderSearchResult;
|
|
11
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
12
|
+
|
|
13
|
+
export function CommandBarItem({ entry, className, active, ...rest }: CommandItemProps) {
|
|
14
|
+
const { handler, displayName: name, icon, iconAlt, keybinding } = entry;
|
|
15
|
+
const firstKeybinding = Array.isArray(keybinding) ? keybinding[0] : keybinding;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
{...rest}
|
|
20
|
+
className={classnames(className, styles.commandBarOption, active && styles.active)}
|
|
21
|
+
onMouseDown={handler}
|
|
22
|
+
>
|
|
23
|
+
{icon && <img src={icon} alt={iconAlt} className={styles.icon} />}
|
|
24
|
+
<div className={styles.name}>{name}</div>
|
|
25
|
+
{firstKeybinding && (
|
|
26
|
+
<KeySequence className={classnames(styles.commandKeys, mutedText)}>{firstKeybinding}</KeySequence>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CommandBarItem, CommandItemProps } from './command-bar-item';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Fuse from 'fuse.js';
|
|
2
|
+
import { CommanderSearchResult, SearchProvider } from '../../types';
|
|
3
|
+
|
|
4
|
+
const searchedKeys: (keyof CommanderSearchResult)[] = ['displayName'];
|
|
5
|
+
|
|
6
|
+
export class CommandSearcher implements SearchProvider {
|
|
7
|
+
private fuseCommands = new Fuse<CommanderSearchResult>([], {
|
|
8
|
+
// weight can be included here.
|
|
9
|
+
// fields loses weight the longer they get, so it seems ok for now.
|
|
10
|
+
keys: searchedKeys,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
constructor(commands: CommanderSearchResult[]) {
|
|
14
|
+
this.fuseCommands.setCollection(commands);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
update(commands: CommanderSearchResult[]) {
|
|
18
|
+
this.fuseCommands.setCollection(commands);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
search(term: string, limit: number): CommanderSearchResult[] {
|
|
22
|
+
const unprefixedPattern = term.replace(/^>/, '');
|
|
23
|
+
const searchResults = this.fuseCommands.search(unprefixedPattern, { limit });
|
|
24
|
+
// @ts-ignore this shows error on Circle for some weird reason
|
|
25
|
+
return searchResults.map((x) => x.item);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test(term: string): boolean {
|
|
29
|
+
return term.startsWith('>');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CommandSearcher } from './command-searcher';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import Fuse from 'fuse.js';
|
|
2
|
+
import memoizeOne from 'memoize-one';
|
|
3
|
+
import { ComponentModel } from '@teambit/component';
|
|
4
|
+
import { SearchProvider, CommanderSearchResult } from '../../types';
|
|
5
|
+
|
|
6
|
+
type ComponentSearchResult = CommanderSearchResult & { name: string };
|
|
7
|
+
|
|
8
|
+
const searchedKeys: (keyof ComponentSearchResult)[] = ['displayName', 'name'];
|
|
9
|
+
|
|
10
|
+
export class ComponentSearcher implements SearchProvider {
|
|
11
|
+
private fuseCommands = new Fuse<ComponentSearchResult>([], {
|
|
12
|
+
// weight can be included here.
|
|
13
|
+
// fields loses weight the longer they get, so it seems ok for now.
|
|
14
|
+
keys: searchedKeys,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
constructor(private navigate: (path: string) => void) {}
|
|
18
|
+
|
|
19
|
+
// this method can be called on every render. memoize to prevent redundant calls
|
|
20
|
+
update = memoizeOne((components: ComponentModel[]) => {
|
|
21
|
+
const { navigate } = this;
|
|
22
|
+
|
|
23
|
+
const searchResults = components.map((c) => ({
|
|
24
|
+
id: c.id.fullName,
|
|
25
|
+
displayName: c.id.fullName,
|
|
26
|
+
name: c.id.name,
|
|
27
|
+
handler: () => navigate(`/${c.id.fullName}`),
|
|
28
|
+
icon: c.environment?.icon,
|
|
29
|
+
iconAlt: c.environment?.id,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
this.fuseCommands.setCollection(searchResults);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
search(term: string, limit: number): ComponentSearchResult[] {
|
|
36
|
+
const searchResults = this.fuseCommands.search(term, { limit });
|
|
37
|
+
// @ts-ignore this shows error on Circle for some weird reason
|
|
38
|
+
return searchResults.map((x) => x.item);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test(term: string): boolean {
|
|
42
|
+
return !term.startsWith('>') && term.length > 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ComponentSearcher } from './component-searcher';
|