Most functions done

master
Vitaliy Filippov 2019-08-30 02:38:35 +03:00
parent bef2601391
commit 72c9cab755
4 changed files with 213 additions and 57 deletions

View File

@ -3,7 +3,7 @@
// For example, a text input with a popup selection list
// ...Or maybe a button with a popup menu
// (c) Vitaliy Filippov 2019+
// Version 2019-08-27
// Version 2019-08-30
import React from 'react';
import ReactDOM from 'react-dom';
@ -15,6 +15,7 @@ export class Picker extends React.Component
direction: PropTypes.string,
clearOnClick: PropTypes.bool,
minWidth: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object,
renderInput: PropTypes.func.isRequired,
renderPicker: PropTypes.func.isRequired,
@ -75,7 +76,7 @@ export class Picker extends React.Component
render()
{
return (<div style={this.props.style}>
return (<div style={this.props.style} className={this.props.className}>
{this.props.renderInput({
onFocus: this.focus,
onBlur: this.blur,

View File

@ -1,16 +1,44 @@
// Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules
// Version 2019-08-27
// Version 2019-08-30
// (c) Vitaliy Filippov 2019+
import React from 'react';
import PropTypes from 'prop-types';
import autocomplete_css from './autocomplete.css';
import input_css from './input.css';
import { Picker } from './Picker.js';
export default class Selectbox extends React.PureComponent
{
static propTypes = {
// multi-select
multiple: PropTypes.bool,
// make text input readonly (disable user input). still allows value change
readOnly: PropTypes.bool,
// show "clear" icon (cross)
allowClear: PropTypes.bool,
// select/autocomplete options
source: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
// current value
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
// change callback
onChange: PropTypes.func,
// item name key - default "name"
labelKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// item id key - default "id"
valueKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
suggestionMatch: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['disabled'])]),
suggestionMsg: PropTypes.any,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
minWidth: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onQueryChange: PropTypes.func,
}
state = {
active: null,
query: null,
@ -19,7 +47,17 @@ export default class Selectbox extends React.PureComponent
setQuery = (ev) =>
{
this.setState({ query: ev.target.value });
const query = ev.target.value;
this.setState({ query });
const f = this.props.onQueryChange;
if (f)
{
f(query);
}
if (!query.length && !this.props.multiple && this.props.allowClear)
{
this.clear();
}
}
onKeyDown = (ev) =>
@ -45,6 +83,25 @@ export default class Selectbox extends React.PureComponent
}
}
clear = () =>
{
this.setState({ query: null });
const f = this.props.onChange;
f && f(null);
}
removeValue = (ev) =>
{
const n = ev.currentTarget.getAttribute('data-n');
if (n != null)
{
const v = [ ...this.props.value ];
v.splice(n, 1);
const f = this.props.onChange;
f && f(v);
}
}
onMouseDown = () =>
{
this.setState({ query: null });
@ -66,7 +123,8 @@ export default class Selectbox extends React.PureComponent
value.splice(already, 1);
}
}
this.props.onChange(value);
const f = this.props.onChange;
f && f(value);
}
onMouseOver = (ev) =>
@ -87,32 +145,76 @@ export default class Selectbox extends React.PureComponent
this.input.focus();
}
onFocus = () =>
{
if (!this.props.multiple && this.state.active === null)
{
const v = this.props.value, vk = this.props.valueKey||'id';
for (let i = 0; i < this.filtered_items.length; i++)
{
if (v == this.filtered_items[i][vk])
{
this.setState({ active: i });
break;
}
}
}
this.picker.focus();
const f = this.props.onFocus;
f && f();
}
onBlur = () =>
{
this.picker.blur();
const f = this.props.onBlur;
f && f();
if (!this.props.multiple && !this.props.allowClear && !this.input.value.length)
{
this.setState({ query: null });
}
}
renderInput = (p) =>
{
const value = this.state.query == null ? (this.props.multiple ? '' : this.item_hash[this.props.value]||'') : this.state.query;
const value = this.state.query == null
? (this.props.multiple ? '' : this.item_hash[this.props.value]||'')
: this.state.query;
const input = <input
readOnly={this.props.readOnly}
placeholder={this.props.multiple ? undefined : this.props.placeholder}
ref={this.setInput}
className={autocomplete_css.inputInputElement}
style={this.props.multiple ? { height: '29px', padding: 0, display: 'inline-block', width: this.state.inputWidth+'px' } : undefined}
onFocus={p.onFocus}
className={autocomplete_css.inputElement}
style={this.props.multiple ? { width: this.state.inputWidth+'px' } : undefined}
onFocus={this.onFocus}
onBlur={this.onBlur}
value={value}
onChange={this.setQuery}
onKeyDown={this.onKeyDown}
/>;
return (<div ref={p.ref} className={autocomplete_css.input}
style={this.props.multiple ? { fontSize: '13px', display: 'flex', overflow: 'hidden', cursor: 'text', height: 'auto' } : undefined}
return (<div ref={p.ref}
className={autocomplete_css.input + (this.props.disabled ? ' '+(autocomplete_css.disabled||'') : '') +
(this.props.multiple
? ' '+(autocomplete_css.multiple||'')
: (this.props.allowClear ? ' '+(autocomplete_css.withClear||'') : ''))}
onClick={this.props.multiple ? this.focusInput : undefined}>
{this.props.multiple
? <div style={{overflow: 'hidden', flex: 1, margin: '0 2em 0 .5em'}}>
<span className={autocomplete_css.inputInputElement} ref={this.setSizer}
style={{display: 'inline-block', width: 'auto', position: 'absolute', padding: '0 2px', top: '-100px', whiteSpace: 'pre', visibility: 'hidden'}}>
? <div className={autocomplete_css.values}>
<span className={autocomplete_css.inputElement+' '+autocomplete_css.sizer} ref={this.setSizer}>
{value}
</span>
{(this.props.value||[]).map(id => <span style={{cursor: 'pointer', display: 'inline-block', borderRadius: '2px', background: '#e0f0ff', border: '1px solid #c0e0ff', padding: '3px', margin: '2px 5px 2px 0'}}>{this.item_hash[id]}</span>)}
{(this.props.value||[]).map((id, idx) => <span className={autocomplete_css.value} key={idx}>
{this.props.allowClear
? <span data-n={idx} className={autocomplete_css.clearValue} onClick={this.removeValue}></span>
: null}
{this.item_hash[id]}
</span>)}
{input}
</div>
: input}
{this.props.allowClear && !this.props.multiple && this.props.value != null
? <div className={autocomplete_css.clear} onClick={this.clear}></div>
: null}
</div>);
}
@ -123,6 +225,7 @@ export default class Selectbox extends React.PureComponent
className={autocomplete_css.suggestion+(this.state.active == i ? ' '+autocomplete_css.active : '')}>
{e[this.props.labelKey||'name']}
</div>))}
{this.props.suggestionMsg}
</div>);
}
@ -183,6 +286,7 @@ export default class Selectbox extends React.PureComponent
clearOnClick={true}
renderInput={this.renderInput}
renderPicker={this.renderSuggestions}
className={this.props.className}
style={this.props.style}
/>);
}

View File

@ -1,18 +1,12 @@
.autocomplete
{
padding: 0;
position: relative;
font-size: inherit;
}
.input
{
background: white;
border: 1px solid #d8d8d8;
border-radius: 0;
font-size: inherit;
font-size: 13px;
line-height: 20px;
padding: 0;
height: 28px;
height: 29px;
position: relative;
}
@ -24,8 +18,8 @@
position: absolute;
right: 1em;
text-align: right;
font-size: 1.5em;
margin-top: -0.375em;
font-size: 20px;
margin-top: -10px;
font-family: FontAwesome;
content: "\f107";
color: #c0c0c0;
@ -33,6 +27,15 @@
cursor: pointer;
}
.input.multiple
{
display: flex;
overflow: hidden;
cursor: text;
height: auto;
padding: 1px 0;
}
.suggestions
{
margin-top: 0;
@ -60,18 +63,7 @@
background: #d6e8f6;
}
.clear
{
top: 4px;
left: 6px;
}
.values
{
padding: 0;
}
.inputInputElement
.inputElement
{
font-size: inherit;
line-height: 200%;
@ -84,47 +76,66 @@
width: 100%;
}
.inputInputElement::-ms-clear
.inputElement::-ms-clear
{
display: none;
}
.inputBar
.multiple .inputElement
{
display: none;
padding: 0;
display: inline-block;
height: 29px;
}
.inputInputElement:focus + .inputBar
.multiple .inputElement.sizer
{
content: " ";
display: block;
border: 1px solid #4196d4;
border-radius: 0;
display: inline-block;
width: auto;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
padding: 0 2px;
top: -100px;
white-space: pre;
visibility: hidden;
}
.inputBar:after, .inputBar:before
.multiple .values
{
display: none;
overflow: hidden;
flex: 1;
margin: 0 2em 0 5px;
line-height: 10px;
}
.withclear input
.multiple .values .value
{
text-indent: 0;
padding-right: 50px;
cursor: pointer;
display: inline-block;
border-radius: 2px;
background: #e0f0ff;
border: 1px solid #c0e0ff;
line-height: 16px;
padding: 3px;
margin: 2px 4px 2px 0;
}
.clear
.withClear .inputElement
{
padding-right: 52px;
}
.clear:after
{
content: "✕";
cursor: pointer;
color: #aaa;
width: 20px;
height: 20px;
border-radius: 10px;
box-sizing: border-box;
border: 1px solid #aaa;
display: block;
position: absolute;
left: auto;
top: 4px;
right: 28px;
@ -133,3 +144,16 @@
text-align: center;
padding: 0;
}
.clearValue:after
{
content: "✕";
color: #444;
cursor: pointer;
display: inline-block;
font-size: 18px;
vertical-align: middle;
margin: 0 3px 0 0;
text-align: center;
padding: 0;
}

27
main.js
View File

@ -14,6 +14,7 @@ class Test extends React.PureComponent
{
state = {
value: [ 'day' ],
value2: 'day',
}
onChange = (v) =>
@ -21,15 +22,41 @@ class Test extends React.PureComponent
this.setState({ value: v });
}
onChange2 = (v) =>
{
this.setState({ value2: v });
}
render()
{
return <div style={{padding: '20px', width: '300px', background: '#e0e8ff'}}>
<Selectbox
source={OPTIONS}
allowClear={true}
multiple={true}
suggestionMatch={true}
value={this.state.value}
style={{marginBottom: '20px'}}
onChange={this.onChange}
/>
<Selectbox
source={OPTIONS}
allowClear={true}
multiple={false}
suggestionMatch={true}
value={this.state.value2}
style={{marginBottom: '20px'}}
onChange={this.onChange2}
/>
<Selectbox
source={OPTIONS}
placeholder="Выберите значение"
allowClear={false}
multiple={false}
suggestionMatch={true}
value={this.state.value2}
onChange={this.onChange2}
/>
</div>;
}
}