Virtual-scroll backed autocomplete for react-toolbox

master
Vitaliy Filippov 2019-06-16 00:04:02 +03:00
commit ce8c6985d6
1 changed files with 198 additions and 0 deletions

198
VirtualTreeAutocomplete.js Normal file
View File

@ -0,0 +1,198 @@
import React from 'react';
import PropTypes from 'prop-types';
import { VirtualScrollList } from 'dynamic-virtual-scroll/VirtualScrollList.js';
import { Spinner24 } from './LoadingOverlay.js';
/**
* Наследование компонентов react-toolbox'а делается, но через такую жопууууу....
*/
import { Input } from 'react-toolbox/lib/input';
import { Chip } from 'react-toolbox/lib/chip';
import { autocompleteFactory } from 'react-toolbox/lib/autocomplete/Autocomplete.js';
import autocomplete_theme from 'react-toolbox/lib/autocomplete/theme.css';
import { themr } from 'react-css-themr';
import { AUTOCOMPLETE } from 'react-toolbox/lib/identifiers.js';
const RawAutocomplete = autocompleteFactory(Chip, Input);
/**
* Автокомплит для отображения БОЛЬШОГО ДЕРЕВА
*/
class RawVirtualTreeAutocomplete extends RawAutocomplete
{
static propTypes = {
...RawAutocomplete.constructor.propTypes,
maxHeight: PropTypes.number,
parentIdField: PropTypes.string.isRequired,
leafOnly: PropTypes.bool,
renderItem: PropTypes.func,
}
tree = []
renderSuggestion = (idx) =>
{
const { theme } = this.props;
const item = this.tree[idx];
if (!item)
{
return null;
}
const key = item.item[this.props.valueKey];
const enabled = !this.props.leafOnly || !this.by_parent[key];
const style = { paddingLeft: (10+item.level*16)+'px', color: enabled ? '' : 'gray' };
if (this.valueHash[key])
{
style.background = '#e5e8ea';
}
let text;
if (this.props.renderItem)
{
text = this.props.renderItem(item.item, item.level, style);
}
else
{
text = item.item[this.props.labelKey];
}
return (<div
id={key}
onMouseDown={enabled ? this.handleMouseDown : undefined}
onMouseOver={enabled ? this.handleSuggestionHover : undefined}
className={theme.suggestion+(this.state.active == key ? ' '+theme.active : '')}
key={key}
style={style}>
{text}
</div>);
}
setListScroll = (e) =>
{
if (e)
{
let k = null;
for (let i in this.valueHash)
{
k = i;
break;
}
if (k)
{
// Если есть значение, при изначальном появлении списка проскроллим к нему
let pos = this.tree.findIndex(e => e.item[this.props.valueKey] == k);
e.scrollToItem(pos);
}
}
}
renderSuggestionList()
{
const { theme } = this.props;
const { top, bottom, maxHeight, left, width } = this.state;
let maxh = Number((''+maxHeight).replace('px', ''));
if (this.props.maxHeight && maxh > this.props.maxHeight)
{
maxh = this.props.maxHeight;
}
return (<div style={{position: 'absolute', top, left, width, maxHeight}}>
<VirtualScrollList
className={theme.suggestions}
style={{maxHeight: maxh+'px', bottom, position: 'absolute'}}
header={((!this.props.source||[]).length ? <div><Spinner24 /> Идёт загрузка...</div> : null)}
totalItems={(this.tree||[]).length}
minRowHeight={36}
viewportHeight={maxh}
renderItem={this.renderSuggestion}
ref={this.setListScroll}
/>
</div>);
}
addItems(parent, level, add_all)
{
for (let item of this.by_parent[parent]||[])
{
if (add_all || !this.filtered || this.filtered[item[this.props.valueKey]])
{
this.tree.push({ item, level });
this.addItems(item[this.props.valueKey], level+1, add_all || this.filtered && this.filtered[item[this.props.valueKey]] == 2);
}
}
}
render()
{
if (this.state.focus)
{
if (this.props.source != this.prevSource)
{
this.state.expanded = {};
const pf = this.props.parentIdField;
const idf = this.props.valueKey;
let by_parent = {};
let by_id = {};
for (let item of this.props.source)
{
by_id[item[idf]] = item;
by_parent[item[pf]||''] = by_parent[item[pf]||''] || [];
by_parent[item[pf]||''].push(item);
}
this.by_id = by_id;
this.by_parent = by_parent;
}
if (this.state.expanded != this.prevExpanded ||
this.state.query != this.prevQuery)
{
if (this.state.query != this.prevQuery ||
this.props.source != this.prevSource)
{
this.filtered = null;
if (this.state.query)
{
const pf = this.props.parentIdField;
this.filtered = {};
for (let k of this.suggestions().keys())
{
this.filtered[k] = 2;
let c = k;
while (c)
{
this.filtered[c] = this.filtered[c] || 1;
if (!this.by_id[c])
break;
c = this.by_id[c][pf];
}
}
}
}
// FIXME Здесь могло быть ваше раскрытие узлов дерева
this.tree = [];
this.addItems('', 0, false);
}
if (!this.valueHash || this.props.value != this.prevValue)
{
if (!this.props.value)
{
this.valueHash = {};
}
else if (this.props.multiple)
{
this.valueHash = (this.props.value instanceof Array
? this.props.value.reduce((a, c) => { a[c] = true; return a; }, {})
: this.props.value);
}
else
{
this.valueHash = { [this.props.value]: true };
}
}
this.prevExpanded = this.state.expanded;
this.prevQuery = this.state.query;
this.prevSource = this.props.source;
this.prevValue = this.props.value;
}
return super.render();
}
}
export const VirtualTreeAutocomplete = themr(AUTOCOMPLETE, autocomplete_theme, { withRef: true })(RawVirtualTreeAutocomplete);