|
@@ -1,107 +1,132 @@
|
|
|
-import React from 'react';
|
|
|
|
|
-import Highlighter from 'react-highlight-words';
|
|
|
|
|
|
|
+import React, { createRef } from 'react';
|
|
|
|
|
+// @ts-ignore
|
|
|
|
|
+import _ from 'lodash';
|
|
|
|
|
+import { FixedSizeList } from 'react-window';
|
|
|
|
|
+
|
|
|
|
|
+import { Themeable, withTheme } from '@grafana/ui';
|
|
|
|
|
|
|
|
import { CompletionItem, CompletionItemGroup } from 'app/types/explore';
|
|
import { CompletionItem, CompletionItemGroup } from 'app/types/explore';
|
|
|
|
|
+import { TypeaheadItem } from './TypeaheadItem';
|
|
|
|
|
+import { TypeaheadInfo } from './TypeaheadInfo';
|
|
|
|
|
+import { flattenGroupItems, calculateLongestLabel, calculateListSizes } from './utils/typeahead';
|
|
|
|
|
|
|
|
-function scrollIntoView(el: HTMLElement) {
|
|
|
|
|
- if (!el || !el.offsetParent) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- const container = el.offsetParent as HTMLElement;
|
|
|
|
|
- if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) {
|
|
|
|
|
- container.scrollTop = el.offsetTop - container.offsetTop;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+interface Props extends Themeable {
|
|
|
|
|
+ groupedItems: CompletionItemGroup[];
|
|
|
|
|
+ menuRef: any;
|
|
|
|
|
+ selectedItem: CompletionItem | null;
|
|
|
|
|
+ onClickItem: (suggestion: CompletionItem) => void;
|
|
|
|
|
+ prefix?: string;
|
|
|
|
|
+ typeaheadIndex: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface TypeaheadItemProps {
|
|
|
|
|
- isSelected: boolean;
|
|
|
|
|
- item: CompletionItem;
|
|
|
|
|
- onClickItem: (Suggestion) => void;
|
|
|
|
|
- prefix?: string;
|
|
|
|
|
|
|
+interface State {
|
|
|
|
|
+ allItems: CompletionItem[];
|
|
|
|
|
+ listWidth: number;
|
|
|
|
|
+ listHeight: number;
|
|
|
|
|
+ itemHeight: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-class TypeaheadItem extends React.PureComponent<TypeaheadItemProps> {
|
|
|
|
|
- el: HTMLElement;
|
|
|
|
|
|
|
+export class Typeahead extends React.PureComponent<Props, State> {
|
|
|
|
|
+ listRef: any = createRef();
|
|
|
|
|
+ documentationRef: any = createRef();
|
|
|
|
|
|
|
|
- componentDidUpdate(prevProps) {
|
|
|
|
|
- if (this.props.isSelected && !prevProps.isSelected) {
|
|
|
|
|
- requestAnimationFrame(() => {
|
|
|
|
|
- scrollIntoView(this.el);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ constructor(props: Props) {
|
|
|
|
|
+ super(props);
|
|
|
|
|
+
|
|
|
|
|
+ const allItems = flattenGroupItems(props.groupedItems);
|
|
|
|
|
+ const longestLabel = calculateLongestLabel(allItems);
|
|
|
|
|
+ const { listWidth, listHeight, itemHeight } = calculateListSizes(props.theme, allItems, longestLabel);
|
|
|
|
|
+ this.state = { listWidth, listHeight, itemHeight, allItems };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- getRef = el => {
|
|
|
|
|
- this.el = el;
|
|
|
|
|
|
|
+ componentDidUpdate = (prevProps: Readonly<Props>) => {
|
|
|
|
|
+ if (prevProps.typeaheadIndex !== this.props.typeaheadIndex && this.listRef && this.listRef.current) {
|
|
|
|
|
+ if (prevProps.typeaheadIndex === 1 && this.props.typeaheadIndex === 0) {
|
|
|
|
|
+ this.listRef.current.scrollToItem(0); // special case for handling the first group label
|
|
|
|
|
+ this.refreshDocumentation();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const index = this.state.allItems.findIndex(item => item === this.props.selectedItem);
|
|
|
|
|
+ this.listRef.current.scrollToItem(index);
|
|
|
|
|
+ this.refreshDocumentation();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (_.isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
|
|
|
|
|
+ const allItems = flattenGroupItems(this.props.groupedItems);
|
|
|
|
|
+ const longestLabel = calculateLongestLabel(allItems);
|
|
|
|
|
+ const { listWidth, listHeight, itemHeight } = calculateListSizes(this.props.theme, allItems, longestLabel);
|
|
|
|
|
+ this.setState({ listWidth, listHeight, itemHeight, allItems }, () => this.refreshDocumentation());
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- onClick = () => {
|
|
|
|
|
- this.props.onClickItem(this.props.item);
|
|
|
|
|
|
|
+ refreshDocumentation = () => {
|
|
|
|
|
+ if (!this.documentationRef.current) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const index = this.state.allItems.findIndex(item => item === this.props.selectedItem);
|
|
|
|
|
+ const item = this.state.allItems[index];
|
|
|
|
|
+
|
|
|
|
|
+ if (item) {
|
|
|
|
|
+ this.documentationRef.current.refresh(item);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- render() {
|
|
|
|
|
- const { isSelected, item, prefix } = this.props;
|
|
|
|
|
- const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
|
|
|
|
|
- const label = item.label || '';
|
|
|
|
|
- return (
|
|
|
|
|
- <li ref={this.getRef} className={className} onClick={this.onClick}>
|
|
|
|
|
- <Highlighter textToHighlight={label} searchWords={[prefix]} highlightClassName="typeahead-match" />
|
|
|
|
|
- {item.documentation && isSelected ? <div className="typeahead-item-hint">{item.documentation}</div> : null}
|
|
|
|
|
- </li>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ onMouseEnter = (item: CompletionItem) => {
|
|
|
|
|
+ this.documentationRef.current.refresh(item);
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
-interface TypeaheadGroupProps {
|
|
|
|
|
- items: CompletionItem[];
|
|
|
|
|
- label: string;
|
|
|
|
|
- onClickItem: (suggestion: CompletionItem) => void;
|
|
|
|
|
- selected: CompletionItem;
|
|
|
|
|
- prefix?: string;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ onMouseLeave = () => {
|
|
|
|
|
+ this.documentationRef.current.hide();
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
-class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps> {
|
|
|
|
|
render() {
|
|
render() {
|
|
|
- const { items, label, selected, onClickItem, prefix } = this.props;
|
|
|
|
|
|
|
+ const { menuRef, selectedItem, onClickItem, prefix, theme } = this.props;
|
|
|
|
|
+ const { listWidth, listHeight, itemHeight, allItems } = this.state;
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <li className="typeahead-group">
|
|
|
|
|
- <div className="typeahead-group__title">{label}</div>
|
|
|
|
|
- <ul className="typeahead-group__list">
|
|
|
|
|
- {items.map(item => {
|
|
|
|
|
|
|
+ <ul className="typeahead" ref={menuRef}>
|
|
|
|
|
+ <TypeaheadInfo
|
|
|
|
|
+ ref={this.documentationRef}
|
|
|
|
|
+ width={listWidth}
|
|
|
|
|
+ height={listHeight}
|
|
|
|
|
+ theme={theme}
|
|
|
|
|
+ initialItem={selectedItem}
|
|
|
|
|
+ />
|
|
|
|
|
+ <FixedSizeList
|
|
|
|
|
+ ref={this.listRef}
|
|
|
|
|
+ itemCount={allItems.length}
|
|
|
|
|
+ itemSize={itemHeight}
|
|
|
|
|
+ itemKey={index => {
|
|
|
|
|
+ const item = allItems && allItems[index];
|
|
|
|
|
+ const key = item ? `${index}-${item.label}` : `${index}`;
|
|
|
|
|
+ return key;
|
|
|
|
|
+ }}
|
|
|
|
|
+ width={listWidth}
|
|
|
|
|
+ height={listHeight}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ index, style }) => {
|
|
|
|
|
+ const item = allItems && allItems[index];
|
|
|
|
|
+ if (!item) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<TypeaheadItem
|
|
<TypeaheadItem
|
|
|
- key={item.label}
|
|
|
|
|
onClickItem={onClickItem}
|
|
onClickItem={onClickItem}
|
|
|
- isSelected={selected === item}
|
|
|
|
|
|
|
+ isSelected={selectedItem === item}
|
|
|
item={item}
|
|
item={item}
|
|
|
prefix={prefix}
|
|
prefix={prefix}
|
|
|
|
|
+ style={style}
|
|
|
|
|
+ onMouseEnter={this.onMouseEnter}
|
|
|
|
|
+ onMouseLeave={this.onMouseLeave}
|
|
|
/>
|
|
/>
|
|
|
);
|
|
);
|
|
|
- })}
|
|
|
|
|
- </ul>
|
|
|
|
|
- </li>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-interface TypeaheadProps {
|
|
|
|
|
- groupedItems: CompletionItemGroup[];
|
|
|
|
|
- menuRef: any;
|
|
|
|
|
- selectedItem: CompletionItem | null;
|
|
|
|
|
- onClickItem: (Suggestion) => void;
|
|
|
|
|
- prefix?: string;
|
|
|
|
|
-}
|
|
|
|
|
-class Typeahead extends React.PureComponent<TypeaheadProps> {
|
|
|
|
|
- render() {
|
|
|
|
|
- const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props;
|
|
|
|
|
- return (
|
|
|
|
|
- <ul className="typeahead" ref={menuRef}>
|
|
|
|
|
- {groupedItems.map(g => (
|
|
|
|
|
- <TypeaheadGroup key={g.label} onClickItem={onClickItem} prefix={prefix} selected={selectedItem} {...g} />
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ }}
|
|
|
|
|
+ </FixedSizeList>
|
|
|
</ul>
|
|
</ul>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export default Typeahead;
|
|
|
|
|
|
|
+export const TypeaheadWithTheme = withTheme(Typeahead);
|