Remove Filter from Chain component (#89)

This commit is contained in:
Maciej Hirsz
2018-10-25 16:45:49 +02:00
committed by GitHub
parent db87eae19f
commit c1155a9c22
8 changed files with 177 additions and 173 deletions
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Types, Maybe } from '@dotstats/common';
import { State as AppState, Node as NodeState } from '../../state';
import { Types } from '@dotstats/common';
import { State as AppState } from '../../state';
import { formatNumber, secondsWithPrecision, getHashData } from '../../utils';
import { Tab, Filter } from './';
import { Tab } from './';
import { Tile, Ago, List, Map, Settings } from '../';
import { PersistentObject, PersistentSet } from '../../persist';
@@ -13,8 +13,6 @@ import listIcon from '../../icons/list-alt-regular.svg';
import worldIcon from '../../icons/location.svg';
import settingsIcon from '../../icons/settings.svg';
const ESCAPE_KEY = 27;
import './Chain.css';
export namespace Chain {
@@ -28,7 +26,6 @@ export namespace Chain {
export interface State {
display: Display;
filter: Maybe<string>;
}
}
@@ -49,18 +46,9 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
this.state = {
display,
filter: null,
};
}
public componentWillMount() {
window.addEventListener('keyup', this.onKeyUp);
}
public componentWillUnmount() {
window.removeEventListener('keyup', this.onKeyUp);
}
public render() {
const { appState } = this.props;
const { best, blockTimestamp, blockAverage } = appState;
@@ -88,7 +76,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
}
private renderContent() {
const { display, filter } = this.state;
const { display } = this.state;
if (display === 'settings') {
return <Settings settings={this.props.settings} />;
@@ -97,57 +85,13 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
const { appState, pins } = this.props;
return (
<React.Fragment>
<Filter value={filter} onChange={this.onFilterChange} />
{
display === 'list'
? <List filter={this.getNodeFilter()} appState={appState} pins={pins} />
: <Map filter={this.getNodeFilter()} appState={appState} />
}
</React.Fragment>
display === 'list'
? <List appState={appState} pins={pins} />
: <Map appState={appState} />
);
}
private setDisplay = (display: Chain.Display) => {
this.setState({ display });
};
private onKeyUp = (event: KeyboardEvent) => {
if (event.ctrlKey) {
return;
}
const { filter } = this.state;
const key = event.key;
const escape = filter != null && event.keyCode === ESCAPE_KEY;
const singleChar = filter == null && key.length === 1;
if (escape) {
this.setState({ filter: null });
} else if (singleChar) {
this.setState({ filter: key });
}
}
private onFilterChange = (filter: string) => {
this.setState({ filter });
}
private getNodeFilter(): Maybe<(node: NodeState) => boolean> {
const { filter } = this.state;
if (filter == null) {
return null;
}
const filterLC = filter.toLowerCase();
return ({ name, city }) => {
const matchesName = name.toLowerCase().indexOf(filterLC) !== -1;
const matchesCity = city != null && city.toLowerCase().indexOf(filterLC) !== -1;
return matchesName || matchesCity;
}
}
}
@@ -1,77 +0,0 @@
import * as React from 'react';
import { Maybe } from '@dotstats/common';
import { Icon } from '../';
import searchIcon from '../../icons/search.svg';
import './Filter.css';
export namespace Filter {
export interface Props {
value: Maybe<string>;
onChange: (value: Maybe<string>) => void;
}
}
const ESCAPE_KEY = 27;
export class Filter extends React.Component<Filter.Props, {}> {
private filterInput: HTMLInputElement;
public componentDidMount() {
this.filterInput.focus();
}
public shouldComponentUpdate(nextProps: Filter.Props): boolean {
if (this.props.value === nextProps.value && this.props.onChange === nextProps.onChange) {
return false;
}
if (this.props.value == null) {
this.filterInput.focus();
}
return true;
}
public render() {
const { value } = this.props;
let className = "Filter";
if (value == null) {
className += " Filter-hidden";
}
return (
<div className={className}>
<Icon src={searchIcon} />
<input ref={this.onRef} value={value || ''} onChange={this.onChange} onKeyUp={this.onKeyUp} onBlur={this.onBlur} />
</div>
);
}
private onRef = (el: HTMLInputElement) => {
this.filterInput = el;
}
private onChange = () => {
const { value } = this.filterInput;
this.props.onChange(value === '' ? null : value);
}
private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
event.stopPropagation();
if (event.keyCode === ESCAPE_KEY) {
this.props.onChange(null);
}
}
private onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
if (this.props.value == null) {
this.filterInput.focus();
}
}
}
@@ -1,3 +1,2 @@
export * from './Chain';
export * from './Tab';
export * from './Filter';
+113
View File
@@ -0,0 +1,113 @@
import * as React from 'react';
import { Maybe } from '@dotstats/common';
import { Node } from '../state';
import { Icon } from './';
import searchIcon from '../icons/search.svg';
import './Filter.css';
export namespace Filter {
export interface Props {
onChange: (value: Maybe<(node: Node) => boolean>) => void;
}
export interface State {
value: string;
}
}
const ESCAPE_KEY = 27;
export class Filter extends React.Component<Filter.Props, {}> {
public state = {
value: ''
};
private filterInput: HTMLInputElement;
public componentWillMount() {
window.addEventListener('keyup', this.onWindowKeyUp);
}
public componentWillUnmount() {
window.removeEventListener('keyup', this.onWindowKeyUp);
}
public shouldComponentUpdate(nextProps: Filter.Props, nextState: Filter.State): boolean {
return this.props.onChange !== nextProps.onChange || this.state.value !== nextState.value;
}
public render() {
const { value } = this.state;
let className = "Filter";
if (value === '') {
className += " Filter-hidden";
}
return (
<div className={className}>
<Icon src={searchIcon} />
<input ref={this.onRef} value={value} onChange={this.onChange} onKeyUp={this.onKeyUp} />
</div>
);
}
private setValue(value: string) {
this.setState({ value });
this.props.onChange(this.getNodeFilter(value));
}
private onRef = (el: HTMLInputElement) => {
this.filterInput = el;
}
private onChange = () => {
this.setValue(this.filterInput.value);
}
private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
event.stopPropagation();
if (event.keyCode === ESCAPE_KEY) {
this.setValue('');
}
}
private onWindowKeyUp = (event: KeyboardEvent) => {
if (event.ctrlKey) {
return;
}
const { value } = this.state;
const key = event.key;
const escape = value && event.keyCode === ESCAPE_KEY;
const singleChar = value === '' && key.length === 1;
if (escape) {
this.setValue('');
} else if (singleChar) {
this.setValue(key);
this.filterInput.focus();
}
}
private getNodeFilter(value: string): Maybe<(node: Node) => boolean> {
if (value === '') {
return null;
}
const filter = value.toLowerCase();
return ({ name, city }) => {
const matchesName = name.toLowerCase().indexOf(filter) !== -1;
const matchesCity = city != null && city.toLowerCase().indexOf(filter) !== -1;
return matchesName || matchesCity;
}
}
}
+26 -13
View File
@@ -1,5 +1,6 @@
import * as React from 'react';
import { Types, Maybe } from '@dotstats/common';
import { Filter } from '../';
import { State as AppState, Node } from '../../state';
import { Row } from './';
import { PersistentSet } from '../../persist';
@@ -14,12 +15,12 @@ import './List.css';
export namespace List {
export interface Props {
filter: Maybe<(node: Node) => boolean>;
appState: Readonly<AppState>;
pins: PersistentSet<Types.NodeName>;
}
export interface State {
filter: Maybe<(node: Node) => boolean>;
viewportHeight: number;
listStart: number;
listEnd: number;
@@ -28,6 +29,7 @@ export namespace List {
export class List extends React.Component<List.Props, {}> {
public state = {
filter: null,
viewportHeight: viewport().height,
listStart: 0,
listEnd: 0,
@@ -50,7 +52,8 @@ export class List extends React.Component<List.Props, {}> {
public render() {
const { settings } = this.props.appState;
const { pins, filter } = this.props;
const { pins } = this.props;
const { filter } = this.state;
const columns = Row.columns.filter(({ setting }) => setting == null || settings[setting]);
let nodes = this.props.appState.nodes.sorted();
@@ -60,7 +63,10 @@ export class List extends React.Component<List.Props, {}> {
if (nodes.length === 0) {
return (
<div className="List List-no-nodes">¯\_()_/¯<br />Nothing matches</div>
<React.Fragment>
<div className="List List-no-nodes">¯\_()_/¯<br />Nothing matches</div>
<Filter onChange={this.onFilterChange} />
</React.Fragment>
);
}
}
@@ -73,16 +79,19 @@ export class List extends React.Component<List.Props, {}> {
nodes = nodes.slice(listStart, listEnd);
return (
<div className="List" style={{ height }}>
<table>
<Row.Header columns={columns} />
<tbody style={{ transform }}>
{
nodes.map((node) => <Row key={node.id} node={node} pins={pins} columns={columns} />)
}
</tbody>
</table>
</div>
<React.Fragment>
<div className="List" style={{ height }}>
<table>
<Row.Header columns={columns} />
<tbody style={{ transform }}>
{
nodes.map((node) => <Row key={node.id} node={node} pins={pins} columns={columns} />)
}
</tbody>
</table>
</div>
<Filter onChange={this.onFilterChange} />
</React.Fragment>
);
}
@@ -123,6 +132,10 @@ export class List extends React.Component<List.Props, {}> {
this.setState({ viewportHeight });
}
private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
this.setState({ filter });
}
}
function divisibleBy(n: number, dividor: number): number {
+30 -19
View File
@@ -1,5 +1,6 @@
import * as React from 'react';
import { Types, Maybe } from '@dotstats/common';
import { Filter } from '../';
import { State as AppState, Node } from '../../state';
import { Location } from './';
import { viewport } from '../../utils';
@@ -12,11 +13,11 @@ import './Map.css';
export namespace Map {
export interface Props {
filter: Maybe<(node: Node) => boolean>;
appState: Readonly<AppState>;
}
export interface State {
filter: Maybe<(node: Node) => boolean>;
width: number;
height: number;
top: number;
@@ -25,7 +26,8 @@ export namespace Map {
}
export class Map extends React.Component<Map.Props, Map.State> {
public state = {
public state: Map.State = {
filter: null,
width: 0,
height: 0,
top: 0,
@@ -43,29 +45,34 @@ export class Map extends React.Component<Map.Props, Map.State> {
}
public render() {
const { filter, appState } = this.props;
const { appState } = this.props;
const { filter } = this.state;
const nodes = appState.nodes.sorted();
return (
<div className="Map">
{
nodes.map((node) => {
const { lat, lon } = node;
const focused = filter == null || filter(node);
<React.Fragment>
<div className="Map">
{
nodes.map((node) => {
const { lat, lon } = node;
if (lat == null || lon == null) {
// Skip nodes with unknown location
return null;
}
const focused = filter == null || filter(node);
const position = this.pixelPosition(lat, lon);
if (lat == null || lon == null) {
// Skip nodes with unknown location
return null;
}
return (
<Location key={node.id} position={position} focused={focused} node={node} />
);
})
}
</div>
const position = this.pixelPosition(lat, lon);
return (
<Location key={node.id} position={position} focused={focused} node={node} />
);
})
}
</div>
<Filter onChange={this.onFilterChange} />
</React.Fragment>
);
}
@@ -115,4 +122,8 @@ export class Map extends React.Component<Map.Props, Map.State> {
this.setState({ top, left, width, height });
}
private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
this.setState({ filter });
}
}
@@ -9,3 +9,4 @@ export * from './Ago';
export * from './OfflineIndicator';
export * from './Sparkline';
export * from './Tooltip';
export * from './Filter';