ReverseSearchInput.js (7927B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 // React & Redux 8 const { 9 Component, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const { 13 connect, 14 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 15 16 const { 17 getReverseSearchTotalResults, 18 getReverseSearchResultPosition, 19 getReverseSearchResult, 20 } = require("resource://devtools/client/webconsole/selectors/history.js"); 21 22 loader.lazyRequireGetter( 23 this, 24 "PropTypes", 25 "resource://devtools/client/shared/vendor/react-prop-types.js" 26 ); 27 loader.lazyRequireGetter( 28 this, 29 "actions", 30 "resource://devtools/client/webconsole/actions/index.js" 31 ); 32 loader.lazyRequireGetter( 33 this, 34 "l10n", 35 "resource://devtools/client/webconsole/utils/messages.js", 36 true 37 ); 38 loader.lazyRequireGetter( 39 this, 40 "PluralForm", 41 "resource://devtools/shared/plural-form.js", 42 true 43 ); 44 loader.lazyRequireGetter( 45 this, 46 "KeyCodes", 47 "resource://devtools/client/shared/keycodes.js", 48 true 49 ); 50 51 const isMacOS = Services.appinfo.OS === "Darwin"; 52 53 class ReverseSearchInput extends Component { 54 static get propTypes() { 55 return { 56 dispatch: PropTypes.func.isRequired, 57 setInputValue: PropTypes.func.isRequired, 58 focusInput: PropTypes.func.isRequired, 59 reverseSearchResult: PropTypes.string, 60 reverseSearchTotalResults: PropTypes.number, 61 reverseSearchResultPosition: PropTypes.number, 62 visible: PropTypes.bool, 63 initialValue: PropTypes.string, 64 }; 65 } 66 67 constructor(props) { 68 super(props); 69 70 this.onInputKeyDown = this.onInputKeyDown.bind(this); 71 } 72 73 componentDidUpdate(prevProps) { 74 const { setInputValue, focusInput } = this.props; 75 if ( 76 prevProps.reverseSearchResult !== this.props.reverseSearchResult && 77 this.props.visible && 78 this.props.reverseSearchTotalResults > 0 79 ) { 80 setInputValue(this.props.reverseSearchResult); 81 } 82 83 if (prevProps.visible === true && this.props.visible === false) { 84 focusInput(); 85 } 86 87 if ( 88 prevProps.visible === false && 89 this.props.visible === true && 90 this.props.initialValue 91 ) { 92 this.inputNode.value = this.props.initialValue; 93 } 94 } 95 96 onEnterKeyboardShortcut(event) { 97 const { dispatch } = this.props; 98 event.stopPropagation(); 99 dispatch(actions.reverseSearchInputToggle()); 100 dispatch(actions.evaluateExpression(undefined, "reverse-search")); 101 } 102 103 onEscapeKeyboardShortcut(event) { 104 const { dispatch } = this.props; 105 event.stopPropagation(); 106 dispatch(actions.reverseSearchInputToggle()); 107 } 108 109 onBackwardNavigationKeyBoardShortcut(event, canNavigate) { 110 const { dispatch } = this.props; 111 event.stopPropagation(); 112 event.preventDefault(); 113 if (canNavigate) { 114 dispatch(actions.showReverseSearchBack({ access: "keyboard" })); 115 } 116 } 117 118 onForwardNavigationKeyBoardShortcut(event, canNavigate) { 119 const { dispatch } = this.props; 120 event.stopPropagation(); 121 event.preventDefault(); 122 if (canNavigate) { 123 dispatch(actions.showReverseSearchNext({ access: "keyboard" })); 124 } 125 } 126 127 onInputKeyDown(event) { 128 const { keyCode, key, ctrlKey, shiftKey } = event; 129 const { reverseSearchTotalResults } = this.props; 130 131 // On Enter, we trigger an execute. 132 if (keyCode === KeyCodes.DOM_VK_RETURN) { 133 return this.onEnterKeyboardShortcut(event); 134 } 135 136 const lowerCaseKey = key.toLowerCase(); 137 138 // On Escape (and Ctrl + c on OSX), we close the reverse search input. 139 if ( 140 keyCode === KeyCodes.DOM_VK_ESCAPE || 141 (isMacOS && ctrlKey && lowerCaseKey === "c") 142 ) { 143 return this.onEscapeKeyboardShortcut(event); 144 } 145 146 const canNavigate = 147 Number.isInteger(reverseSearchTotalResults) && 148 reverseSearchTotalResults > 1; 149 150 if ( 151 (!isMacOS && key === "F9" && !shiftKey) || 152 (isMacOS && ctrlKey && lowerCaseKey === "r") 153 ) { 154 return this.onBackwardNavigationKeyBoardShortcut(event, canNavigate); 155 } 156 157 if ( 158 (!isMacOS && key === "F9" && shiftKey) || 159 (isMacOS && ctrlKey && lowerCaseKey === "s") 160 ) { 161 return this.onForwardNavigationKeyBoardShortcut(event, canNavigate); 162 } 163 164 return null; 165 } 166 167 renderSearchInformation() { 168 const { reverseSearchTotalResults, reverseSearchResultPosition } = 169 this.props; 170 171 if (!Number.isInteger(reverseSearchTotalResults)) { 172 return null; 173 } 174 175 let text; 176 if (reverseSearchTotalResults === 0) { 177 text = l10n.getStr("webconsole.reverseSearch.noResult"); 178 } else { 179 const resultsString = l10n.getStr("webconsole.reverseSearch.results"); 180 text = PluralForm.get(reverseSearchTotalResults, resultsString) 181 .replace("#1", reverseSearchResultPosition) 182 .replace("#2", reverseSearchTotalResults); 183 } 184 185 return dom.div({ className: "reverse-search-info" }, text); 186 } 187 188 renderNavigationButtons() { 189 const { dispatch, reverseSearchTotalResults } = this.props; 190 191 if ( 192 !Number.isInteger(reverseSearchTotalResults) || 193 reverseSearchTotalResults <= 1 194 ) { 195 return null; 196 } 197 198 return [ 199 dom.button({ 200 key: "search-result-button-prev", 201 className: "devtools-button search-result-button-prev", 202 title: l10n.getFormatStr( 203 "webconsole.reverseSearch.result.previousButton.tooltip", 204 [isMacOS ? "Ctrl + R" : "F9"] 205 ), 206 onClick: () => { 207 dispatch(actions.showReverseSearchBack({ access: "click" })); 208 this.inputNode.focus(); 209 }, 210 }), 211 dom.button({ 212 key: "search-result-button-next", 213 className: "devtools-button search-result-button-next", 214 title: l10n.getFormatStr( 215 "webconsole.reverseSearch.result.nextButton.tooltip", 216 [isMacOS ? "Ctrl + S" : "Shift + F9"] 217 ), 218 onClick: () => { 219 dispatch(actions.showReverseSearchNext({ access: "click" })); 220 this.inputNode.focus(); 221 }, 222 }), 223 ]; 224 } 225 226 render() { 227 const { dispatch, visible, reverseSearchTotalResults } = this.props; 228 229 if (!visible) { 230 return null; 231 } 232 233 const classNames = ["reverse-search"]; 234 235 if (reverseSearchTotalResults === 0) { 236 classNames.push("no-result"); 237 } 238 239 return dom.div( 240 { className: classNames.join(" ") }, 241 dom.input({ 242 ref: node => { 243 this.inputNode = node; 244 }, 245 autoFocus: true, 246 placeholder: l10n.getStr("webconsole.reverseSearch.input.placeHolder"), 247 className: "reverse-search-input devtools-monospace", 248 onKeyDown: this.onInputKeyDown, 249 onInput: ({ target }) => 250 dispatch(actions.reverseSearchInputChange(target.value)), 251 }), 252 dom.div( 253 { 254 className: "reverse-search-actions", 255 }, 256 this.renderSearchInformation(), 257 this.renderNavigationButtons(), 258 dom.button({ 259 className: "devtools-button reverse-search-close-button", 260 title: l10n.getFormatStr( 261 "webconsole.reverseSearch.closeButton.tooltip", 262 ["Esc" + (isMacOS ? " | Ctrl + C" : "")] 263 ), 264 onClick: () => { 265 dispatch(actions.reverseSearchInputToggle()); 266 }, 267 }) 268 ) 269 ); 270 } 271 } 272 273 const mapStateToProps = state => ({ 274 visible: state.ui.reverseSearchInputVisible, 275 reverseSearchTotalResults: getReverseSearchTotalResults(state), 276 reverseSearchResultPosition: getReverseSearchResultPosition(state), 277 reverseSearchResult: getReverseSearchResult(state), 278 }); 279 280 const mapDispatchToProps = dispatch => ({ dispatch }); 281 282 module.exports = connect( 283 mapStateToProps, 284 mapDispatchToProps 285 )(ReverseSearchInput);