ViewportDimension.js (7226B)
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 const { 8 PureComponent, 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 const { 14 isKeyIn, 15 } = require("resource://devtools/client/responsive/utils/key.js"); 16 const { 17 MIN_VIEWPORT_DIMENSION, 18 } = require("resource://devtools/client/responsive/constants.js"); 19 const Types = require("resource://devtools/client/responsive/types.js"); 20 21 loader.lazyRequireGetter( 22 this, 23 "KeyCodes", 24 "resource://devtools/client/shared/keycodes.js", 25 true 26 ); 27 28 class ViewportDimension extends PureComponent { 29 static get propTypes() { 30 return { 31 doResizeViewport: PropTypes.func.isRequired, 32 onRemoveDeviceAssociation: PropTypes.func, 33 viewport: PropTypes.shape(Types.viewport).isRequired, 34 }; 35 } 36 37 static getDerivedStateFromProps(props, state) { 38 const { width, height } = props.viewport; 39 if (state.prevWidth !== width || state.prevHeight !== height) { 40 return { 41 width, 42 height, 43 prevWidth: width, 44 prevHeight: height, 45 }; 46 } 47 return null; 48 } 49 50 constructor(props) { 51 super(props); 52 53 const { width, height } = props.viewport; 54 55 this.state = { 56 width, 57 height, 58 prevWidth: width, 59 prevHeight: height, 60 isEditing: false, 61 isWidthValid: true, 62 isHeightValid: true, 63 }; 64 65 this.isInputValid = this.isInputValid.bind(this); 66 this.onInputBlur = this.onInputBlur.bind(this); 67 this.onInputChange = this.onInputChange.bind(this); 68 this.onInputFocus = this.onInputFocus.bind(this); 69 this.onInputKeyDown = this.onInputKeyDown.bind(this); 70 this.onInputKeyUp = this.onInputKeyUp.bind(this); 71 this.onInputSubmit = this.onInputSubmit.bind(this); 72 } 73 74 /** 75 * Return true if the given value is a number and greater than MIN_VIEWPORT_DIMENSION 76 * and false otherwise. 77 */ 78 isInputValid(value) { 79 return ( 80 /^\d{2,4}$/.test(value) && parseInt(value, 10) >= MIN_VIEWPORT_DIMENSION 81 ); 82 } 83 84 onInputBlur() { 85 const { width, height } = this.props.viewport; 86 87 if (this.state.width != width || this.state.height != height) { 88 this.onInputSubmit(); 89 } 90 91 this.setState({ isEditing: false }); 92 } 93 94 onInputChange({ target }, callback) { 95 if (target.value.length > 4) { 96 return; 97 } 98 99 if (this.widthInput == target) { 100 this.setState( 101 { 102 width: target.value, 103 isWidthValid: this.isInputValid(target.value), 104 }, 105 callback 106 ); 107 } 108 109 if (this.heightInput == target) { 110 this.setState( 111 { 112 height: target.value, 113 isHeightValid: this.isInputValid(target.value), 114 }, 115 callback 116 ); 117 } 118 } 119 120 onInputFocus(e) { 121 this.setState({ isEditing: true }); 122 e.target.select(); 123 } 124 125 onInputKeyDown(event) { 126 const increment = getIncrement(event); 127 if (!increment) { 128 return; 129 } 130 131 const { target } = event; 132 target.value = parseInt(target.value, 10) + increment; 133 this.onInputChange(event, this.onInputSubmit); 134 135 // Keep this event from having default processing. Since the field is a 136 // number field, default processing would trigger additional manipulations 137 // of the value, and we've already applied the desired amount. 138 event.preventDefault(); 139 } 140 141 onInputKeyUp({ target, keyCode }) { 142 // On Enter, submit the input 143 if (keyCode == KeyCodes.DOM_VK_RETURN) { 144 this.onInputSubmit(); 145 } 146 147 // On Esc, revert the value and blur the target 148 if (keyCode == KeyCodes.DOM_VK_ESCAPE) { 149 if (this.widthInput == target) { 150 const width = this.props.viewport.width; 151 this.setState( 152 { 153 width, 154 isWidthValid: this.isInputValid(width), 155 }, 156 () => target.blur() 157 ); 158 } 159 160 if (this.heightInput == target) { 161 const height = this.props.viewport.height; 162 this.setState( 163 { 164 height, 165 isHeightValid: this.isInputValid(height), 166 }, 167 () => target.blur() 168 ); 169 } 170 } 171 } 172 173 onInputSubmit() { 174 const { viewport, onRemoveDeviceAssociation, doResizeViewport } = 175 this.props; 176 177 if (!this.state.isWidthValid || !this.state.isHeightValid) { 178 const { width, height } = viewport; 179 180 this.setState({ 181 width, 182 height, 183 isWidthValid: true, 184 isHeightValid: true, 185 }); 186 187 return; 188 } 189 190 // Change the device selector back to an unselected device 191 // TODO: Bug 1332754: Logic like this probably belongs in the action creator. 192 if (viewport.device && typeof onRemoveDeviceAssociation === "function") { 193 onRemoveDeviceAssociation(viewport.id, { resetProfile: false }); 194 } 195 196 doResizeViewport( 197 viewport.id, 198 parseInt(this.state.width, 10), 199 parseInt(this.state.height, 10) 200 ); 201 } 202 203 render() { 204 return dom.div( 205 { 206 className: 207 "viewport-dimension" + 208 (this.state.isEditing ? " editing" : "") + 209 (!this.state.isWidthValid || !this.state.isHeightValid 210 ? " invalid" 211 : ""), 212 }, 213 dom.input({ 214 ref: input => { 215 this.widthInput = input; 216 }, 217 className: 218 "text-input viewport-dimension-input" + 219 (this.state.isWidthValid ? "" : " invalid"), 220 size: 4, 221 type: "number", 222 value: this.state.width, 223 onBlur: this.onInputBlur, 224 onChange: this.onInputChange, 225 onFocus: this.onInputFocus, 226 onKeyDown: this.onInputKeyDown, 227 onKeyUp: this.onInputKeyUp, 228 }), 229 dom.span( 230 { 231 className: "viewport-dimension-separator", 232 }, 233 "×" 234 ), 235 dom.input({ 236 ref: input => { 237 this.heightInput = input; 238 }, 239 className: 240 "text-input viewport-dimension-input" + 241 (this.state.isHeightValid ? "" : " invalid"), 242 size: 4, 243 type: "number", 244 value: this.state.height, 245 onBlur: this.onInputBlur, 246 onChange: this.onInputChange, 247 onFocus: this.onInputFocus, 248 onKeyDown: this.onInputKeyDown, 249 onKeyUp: this.onInputKeyUp, 250 }) 251 ); 252 } 253 } 254 255 /** 256 * Get the increment/decrement step to use for the provided key event. 257 */ 258 function getIncrement(event) { 259 const defaultIncrement = 1; 260 const largeIncrement = 100; 261 const mediumIncrement = 10; 262 263 let increment = 0; 264 const key = event.keyCode; 265 266 if (isKeyIn(key, "UP", "PAGE_UP")) { 267 increment = 1 * defaultIncrement; 268 } else if (isKeyIn(key, "DOWN", "PAGE_DOWN")) { 269 increment = -1 * defaultIncrement; 270 } 271 272 if (event.shiftKey) { 273 if (isKeyIn(key, "PAGE_UP", "PAGE_DOWN")) { 274 increment *= largeIncrement; 275 } else { 276 increment *= mediumIncrement; 277 } 278 } 279 280 return increment; 281 } 282 283 module.exports = ViewportDimension;