SearchBoxAutocompletePopup.js (4424B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 Component, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 13 class SearchBoxAutocompletePopup extends Component { 14 static get propTypes() { 15 return { 16 /** 17 * autocompleteProvider takes search-box's entire input text as `filter` argument 18 * ie. "is:cached pr" 19 * returned value is array of objects like below 20 * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]] 21 * `value` is used to update the search-box input box for given item 22 * `displayValue` is used to render the autocomplete list 23 */ 24 autocompleteProvider: PropTypes.func.isRequired, 25 filter: PropTypes.string.isRequired, 26 onItemSelected: PropTypes.func.isRequired, 27 }; 28 } 29 30 static computeState({ autocompleteProvider, filter }) { 31 const list = autocompleteProvider(filter); 32 const selectedIndex = list.length ? 0 : -1; 33 34 return { list, selectedIndex, prevFilter: filter }; 35 } 36 37 static getDerivedStateFromProps(props, state) { 38 if (props.filter !== state.prevFilter) { 39 return SearchBoxAutocompletePopup.computeState(props); 40 } 41 return null; 42 } 43 44 constructor(props, context) { 45 super(props, context); 46 this.state = SearchBoxAutocompletePopup.computeState(props); 47 this.jumpToTop = this.jumpToTop.bind(this); 48 this.jumpToBottom = this.jumpToBottom.bind(this); 49 this.jumpBy = this.jumpBy.bind(this); 50 this.select = this.select.bind(this); 51 this.onMouseDown = this.onMouseDown.bind(this); 52 } 53 54 componentDidUpdate() { 55 if (this.refs.selected) { 56 this.refs.selected.scrollIntoView(false); 57 } 58 } 59 60 /** 61 * Use this method to select the top-most item 62 * This method is public, called outside of the autocomplete-popup component. 63 */ 64 jumpToTop() { 65 this.setState({ selectedIndex: 0 }); 66 } 67 68 /** 69 * Use this method to select the bottom-most item 70 * This method is public. 71 */ 72 jumpToBottom() { 73 this.setState({ selectedIndex: this.state.list.length - 1 }); 74 } 75 76 /** 77 * Increment the selected index with the provided increment value. Will cycle to the 78 * beginning/end of the list if the index exceeds the list boundaries. 79 * This method is public. 80 * 81 * @param {number} increment - No. of hops in the direction 82 */ 83 jumpBy(increment = 1) { 84 const { list, selectedIndex } = this.state; 85 let nextIndex = selectedIndex + increment; 86 if (increment > 0) { 87 // Positive cycling 88 nextIndex = nextIndex > list.length - 1 ? 0 : nextIndex; 89 } else if (increment < 0) { 90 // Inverse cycling 91 nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex; 92 } 93 this.setState({ selectedIndex: nextIndex }); 94 } 95 96 /** 97 * Submit the currently selected item to the onItemSelected callback 98 * This method is public. 99 */ 100 select() { 101 if (this.refs.selected) { 102 this.props.onItemSelected(this.refs.selected.dataset.value); 103 } 104 } 105 106 onMouseDown(e) { 107 e.preventDefault(); 108 this.setState( 109 { selectedIndex: Number(e.target.dataset.index) }, 110 this.select 111 ); 112 } 113 114 render() { 115 const { list } = this.state; 116 117 return ( 118 !!list.length && 119 dom.div( 120 { className: "devtools-autocomplete-popup devtools-monospace" }, 121 dom.ul( 122 { className: "devtools-autocomplete-listbox" }, 123 list.map((item, i) => { 124 const isSelected = this.state.selectedIndex == i; 125 const itemClassList = ["autocomplete-item"]; 126 127 if (isSelected) { 128 itemClassList.push("autocomplete-selected"); 129 } 130 return dom.li( 131 { 132 key: i, 133 "data-index": i, 134 "data-value": item.value, 135 className: itemClassList.join(" "), 136 ref: isSelected ? "selected" : null, 137 onMouseDown: this.onMouseDown, 138 }, 139 item.displayValue 140 ); 141 }) 142 ) 143 ) 144 ); 145 } 146 } 147 148 module.exports = SearchBoxAutocompletePopup;