From ce8c6985d6f158f8a1c1d7e6b2aea3de6e7489be Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 16 Jun 2019 00:04:02 +0300 Subject: [PATCH] Virtual-scroll backed autocomplete for react-toolbox --- VirtualTreeAutocomplete.js | 198 +++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 VirtualTreeAutocomplete.js diff --git a/VirtualTreeAutocomplete.js b/VirtualTreeAutocomplete.js new file mode 100644 index 0000000..d84858d --- /dev/null +++ b/VirtualTreeAutocomplete.js @@ -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 (
+ {text} +
); + } + + 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 (
+ Идёт загрузка...
: null)} + totalItems={(this.tree||[]).length} + minRowHeight={36} + viewportHeight={maxh} + renderItem={this.renderSuggestion} + ref={this.setListScroll} + /> + ); + } + + 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);