@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.
@@ -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
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@teambit/command-bar",
3
- "version": "0.0.566",
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.566"
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.566",
23
- "@teambit/pubsub": "0.0.566",
24
- "@teambit/ui-foundation.ui.is-browser": "0.0.470",
25
- "@teambit/ui": "0.0.566",
26
- "@teambit/bit-error": "0.0.379",
27
- "@teambit/ui-foundation.ui.constants.z-indexes": "0.0.471",
28
- "@teambit/ui-foundation.ui.keycap": "0.0.470",
29
- "@teambit/component": "0.0.566"
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.179",
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.179",
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"
@@ -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
+ }
@@ -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,4 @@
1
+ import { createContext } from 'react';
2
+ import { CommandBarUI } from '../../command-bar.ui.runtime';
3
+
4
+ export const CommandBarContext = createContext<CommandBarUI | undefined>(undefined);
@@ -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';