diff --git a/packages/react/demo/index.tsx b/packages/react/demo/index.tsx index 7f03359..5cc330c 100644 --- a/packages/react/demo/index.tsx +++ b/packages/react/demo/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' -import { Select2, Select2Option, Select2Data, Select2Group, Select2UpdateValue } from '../dist/' +import { Select2, Select2Option, Select2Data, Select2Group, Select2UpdateValue, AutoComplete } from '../dist/' import { data1, data2, data3, data5, data12 } from 'select2-component/demo/' const CustomOption: React.StatelessComponent<{ option: Select2Option }> = props => <span>{props.option.label}<span style={{ float: 'right', color: 'red' }}>{props.option.value}</span></span> @@ -28,6 +28,24 @@ class Main extends React.Component<{}, {}> { private data8 = data8 private data9: Select2Data = JSON.parse(JSON.stringify(data1)) private data12 = data12 + private get data13(): Select2Data { + return this.value13 + ? [ + { + value: this.value13, + label: this.value13, + }, + { + value: this.value13 + this.value13, + label: this.value13 + this.value13, + }, + { + value: this.value13 + this.value13 + this.value13, + label: this.value13 + this.value13 + this.value13, + }, + ] + : [] + } private value1 = 'CA' private value2 = 'CA' @@ -39,6 +57,7 @@ class Main extends React.Component<{}, {}> { private value8 = 'CA' private value9: string[] = [] private value12 = true + private value13 = '' render() { return ( @@ -99,6 +118,12 @@ class Main extends React.Component<{}, {}> { value={this.value12} update={value => this.update12(value)}> </Select2> + <h3>auto complete ({this.value13})</h3> + <AutoComplete data={this.data13} + value={this.value13} + search={value => this.update13(value)} + select={value => this.update13(value)}> + </AutoComplete> </div> ) } @@ -152,6 +177,10 @@ class Main extends React.Component<{}, {}> { private keydown(e: React.KeyboardEvent) { console.info(e.key) } + private update13(value: Select2UpdateValue) { + this.value13 = value as string + this.setState({ value13: this.value13 }) + } } ReactDOM.render(<Main />, document.getElementById('container')) diff --git a/packages/react/src/auto-complete.tsx b/packages/react/src/auto-complete.tsx new file mode 100644 index 0000000..e686d2f --- /dev/null +++ b/packages/react/src/auto-complete.tsx @@ -0,0 +1,300 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import * as common from 'select2-component' +export * from 'select2-component' + +/** + * @public + */ +export class AutoComplete extends React.PureComponent<{ + data: common.Select2Data; + value: string; + update?: (value: common.Select2UpdateValue) => void; + search?: (text: string) => void; + select?: (text: common.Select2UpdateValue) => void; + keydown?: (e: React.KeyboardEvent) => void; + keyup?: (e: React.KeyboardEvent) => void; + keypress?: (e: React.KeyboardEvent) => void; +}, {}> { + private hoveringValue?: common.Select2Value | null = null + private option: common.Select2Option | common.Select2Option[] | null = null + private isOpen = false + private focusoutTimer?: NodeJS.Timer + private lastScrollTopIndex = 0 + + private searchInputElement!: HTMLElement + private resultsElement!: HTMLElement + + private get dropdownStyle() { + return common.getDropdownStyle(this.isOpen && this.props.data.length > 0) + } + + private get containerStyle() { + return common.getContainerStyle(false, this.isOpen && this.props.data.length > 0) + } + + componentWillMount() { + const option = common.getOptionsByValue(this.props.data, this.props.value, false) + if (option !== null) { + this.option = option + this.setState({ option: this.option }) + } + if (!Array.isArray(option)) { + this.hoveringValue = this.props.value as string | undefined + } + this.setState({ hoveringValue: this.hoveringValue }) + } + + componentDidMount() { + const theElement = ReactDOM.findDOMNode(this as any) as HTMLElement + this.searchInputElement = theElement.childNodes[0].childNodes[0].childNodes[0].childNodes[0] as HTMLElement + this.resultsElement = theElement.childNodes[1].childNodes[0].childNodes[0].childNodes[0] as HTMLElement + } + + render() { + const results = this.renderResult() + return ( + <div className={this.containerStyle}> + <div className='selection'> + <div className='select2-search select2-search--dropdown'> + <input value={this.props.value} + onChange={this.onChange} + onKeyDown={e => this.keyDown(e)} + onKeyUp={e => this.keyUp(e)} + onKeyPress={e => this.keyPress(e)} + onBlur={() => this.focusout()} + onClick={() => this.toggleOpenAndClose()} + className='select2-search__field' + type='search' + role='textbox' + autoComplete='off' + autoCorrect='off' + autoCapitalize='off' + spellCheck={false} /> + </div> + </div> + <div className={this.dropdownStyle}> + <div className='select2-dropdown'> + <div className='select2-results'> + <ul className='select2-results__options' + role='tree' + tabIndex={-1} + onKeyDown={e => this.keyDown(e)} + > + {results} + </ul> + </div> + </div> + </div> + </div> + ) + } + + private renderResult() { + return this.getFilteredData(false).map((groupOrOption, i) => { + const options = (groupOrOption as common.Select2Group).options + if (options) { + const optionsElements = options.map((option, j) => { + const optionElement = option.component + ? React.createElement(option.component as React.ComponentClass<{ option: common.Select2Option }>, { option }) + : option.label + return ( + <li className={this.getOptionStyle(option.value)} + key={j} + role='treeitem' + aria-selected={this.isSelected(option)} + aria-disabled={this.isDisabled(option)} + onMouseEnter={() => this.mouseenter(option)} + onClick={() => this.click(option)}> + {optionElement} + </li> + ) + }) + return ( + <li className='select2-results__option' role='group' key={i}> + <strong className='select2-results__group'>{groupOrOption.label}</strong> + <ul className='select2-results__options select2-results__options--nested'> + {optionsElements} + </ul> + </li> + ) + } else { + const option = groupOrOption as common.Select2Option + const optionElement = option.component + ? React.createElement(option.component as React.ComponentClass<{ option: common.Select2Option }>, { option }) + : option.label + return ( + <li className={this.getOptionStyle(option.value)} + key={i} + role='treeitem' + aria-selected={this.isSelected(option)} + aria-disabled={this.isDisabled(option)} + onMouseEnter={() => this.mouseenter(option)} + onClick={() => this.click(option)}> + {optionElement} + </li> + ) + } + }) + } + + private getFilteredData(canSetState: boolean) { + const result = this.props.data + + if (common.valueIsNotInFilteredData(result, this.hoveringValue)) { + this.hoveringValue = common.getFirstAvailableOption(result) + if (canSetState) { + this.setState({ hoveringValue: this.hoveringValue }) + } + + if (this.resultsElement) { + const lastScrollTopIndex = common.getLastScrollTopIndex(this.hoveringValue, this.resultsElement, result, this.lastScrollTopIndex) + if (lastScrollTopIndex !== null) { + this.lastScrollTopIndex = lastScrollTopIndex + if (canSetState) { + this.setState({ lastScrollTopIndex: this.lastScrollTopIndex }) + } + } + } + } + return result + } + private getOptionStyle(value: common.Select2Value) { + return common.getOptionStyle(value, this.hoveringValue) + } + private mouseenter(option: common.Select2Option) { + if (!option.disabled) { + this.hoveringValue = option.value + this.setState({ hoveringValue: this.hoveringValue }) + } + } + private click(option: common.Select2Option) { + if (!option.disabled) { + this.select(option) + } + if (this.focusoutTimer) { + clearTimeout(this.focusoutTimer) + } + } + private toggleOpenAndClose() { + this.isOpen = !this.isOpen + this.setState({ isOpen: this.isOpen }) + if (this.isOpen) { + this.focusSearchboxOrResultsElement() + + if (this.resultsElement) { + const lastScrollTopIndex = common.getLastScrollTopIndex(this.hoveringValue, this.resultsElement, this.props.data, this.lastScrollTopIndex) + if (lastScrollTopIndex !== null) { + this.lastScrollTopIndex = lastScrollTopIndex + } + } + } + if (this.focusoutTimer) { + clearTimeout(this.focusoutTimer) + } + } + private focusout() { + this.focusoutTimer = setTimeout(() => { + this.isOpen = false + this.setState({ isOpen: this.isOpen }) + this.focusoutTimer = undefined + }, common.timeout) + } + private moveUp() { + this.hoveringValue = common.getPreviousOption(this.getFilteredData(true), this.hoveringValue) + this.setState({ hoveringValue: this.hoveringValue }) + + if (this.resultsElement) { + const lastScrollTopIndex = common.getLastScrollTopIndex(this.hoveringValue, this.resultsElement, this.getFilteredData(true), this.lastScrollTopIndex) + if (lastScrollTopIndex !== null) { + this.lastScrollTopIndex = lastScrollTopIndex + this.setState({ lastScrollTopIndex: this.lastScrollTopIndex }) + } + } + } + private moveDown() { + this.hoveringValue = common.getNextOption(this.getFilteredData(true), this.hoveringValue) + this.setState({ hoveringValue: this.hoveringValue }) + + if (this.resultsElement) { + const lastScrollTopIndex = common.getLastScrollTopIndex(this.hoveringValue, this.resultsElement, this.getFilteredData(true), this.lastScrollTopIndex) + if (lastScrollTopIndex !== null) { + this.lastScrollTopIndex = lastScrollTopIndex + this.setState({ lastScrollTopIndex: this.lastScrollTopIndex }) + } + } + } + private selectByEnter() { + if (this.hoveringValue) { + const option = common.getOptionByValue(this.props.data, this.hoveringValue) + this.select(option) + } + } + private select(option: common.Select2Option | null) { + if (option !== null) { + this.option = option + this.isOpen = false + this.setState({ + option: this.option, + isOpen: this.isOpen + }) + } + + if (this.props.select) { + this.props.select((this.option as common.Select2Option).value) + } + if (this.props.update) { + this.props.update((this.option as common.Select2Option).value) + } + } + + private keyDown(e: React.KeyboardEvent<HTMLInputElement> | React.KeyboardEvent<HTMLUListElement>) { + if (this.props.keydown) { + this.props.keydown(e) + } + if (e.keyCode === 40) { + this.moveDown() + e.preventDefault() + } else if (e.keyCode === 38) { + this.moveUp() + e.preventDefault() + } else if (e.keyCode === 13) { + this.selectByEnter() + e.preventDefault() + } + } + + private keyUp(e: React.KeyboardEvent<HTMLInputElement> | React.KeyboardEvent<HTMLUListElement>) { + if (this.props.keyup) { + this.props.keyup(e) + } + } + + private keyPress(e: React.KeyboardEvent<HTMLInputElement> | React.KeyboardEvent<HTMLUListElement>) { + if (this.props.keypress) { + this.props.keypress(e) + } + } + + private onChange = (e: React.FormEvent<{ value: string }>) => { + if (this.props.search) { + this.props.search(e.currentTarget.value) + } + if (this.props.update) { + this.props.update(e.currentTarget.value) + } + } + + private isSelected(option: common.Select2Option) { + return common.isSelected(this.option, option, false) + } + private isDisabled(option: common.Select2Option) { + return option.disabled ? 'true' : 'false' + } + + private focusSearchboxOrResultsElement() { + if (this.searchInputElement) { + this.searchInputElement.focus() + } + } +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000..964313d --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,2 @@ +export * from './select2' +export * from './auto-complete' diff --git a/packages/react/src/index.tsx b/packages/react/src/select2.tsx similarity index 100% rename from packages/react/src/index.tsx rename to packages/react/src/select2.tsx