1/* 2 * Copyright (C) 2013 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31/** 32 * @interface 33 */ 34WebInspector.SuggestBoxDelegate = function() 35{ 36} 37 38WebInspector.SuggestBoxDelegate.prototype = { 39 /** 40 * @param {string} suggestion 41 * @param {boolean=} isIntermediateSuggestion 42 */ 43 applySuggestion: function(suggestion, isIntermediateSuggestion) { }, 44 45 /** 46 * acceptSuggestion will be always called after call to applySuggestion with isIntermediateSuggestion being equal to false. 47 */ 48 acceptSuggestion: function() { }, 49} 50 51/** 52 * @constructor 53 * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate 54 * @param {number=} maxItemsHeight 55 */ 56WebInspector.SuggestBox = function(suggestBoxDelegate, maxItemsHeight) 57{ 58 this._suggestBoxDelegate = suggestBoxDelegate; 59 this._length = 0; 60 this._selectedIndex = -1; 61 this._selectedElement = null; 62 this._maxItemsHeight = maxItemsHeight; 63 this._bodyElement = document.body; 64 this._maybeHideBound = this._maybeHide.bind(this); 65 this._element = document.createElement("div"); 66 this._element.className = "suggest-box"; 67 this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true); 68} 69 70WebInspector.SuggestBox.prototype = { 71 /** 72 * @return {boolean} 73 */ 74 visible: function() 75 { 76 return !!this._element.parentElement; 77 }, 78 79 /** 80 * @param {!AnchorBox} anchorBox 81 */ 82 setPosition: function(anchorBox) 83 { 84 this._updateBoxPosition(anchorBox); 85 }, 86 87 /** 88 * @param {!AnchorBox} anchorBox 89 */ 90 _updateBoxPosition: function(anchorBox) 91 { 92 console.assert(this._overlay); 93 if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox)) 94 return; 95 this._lastAnchorBox = anchorBox; 96 97 // Position relative to main DevTools element. 98 var container = WebInspector.Dialog.modalHostView().element; 99 anchorBox = anchorBox.relativeToElement(container); 100 var totalWidth = container.offsetWidth; 101 var totalHeight = container.offsetHeight; 102 var aboveHeight = anchorBox.y; 103 var underHeight = totalHeight - anchorBox.y - anchorBox.height; 104 105 var rowHeight = 17; 106 const spacer = 6; 107 108 var maxHeight = this._maxItemsHeight ? this._maxItemsHeight * rowHeight : Math.max(underHeight, aboveHeight) - spacer; 109 var under = underHeight >= aboveHeight; 110 this._leftSpacerElement.style.flexBasis = anchorBox.x + "px"; 111 112 this._overlay.element.classList.toggle("under-anchor", under); 113 114 if (under) { 115 this._bottomSpacerElement.style.flexBasis = "auto"; 116 this._topSpacerElement.style.flexBasis = (anchorBox.y + anchorBox.height) + "px"; 117 } else { 118 this._bottomSpacerElement.style.flexBasis = (totalHeight - anchorBox.y) + "px"; 119 this._topSpacerElement.style.flexBasis = "auto"; 120 } 121 this._element.style.maxHeight = maxHeight + "px"; 122 }, 123 124 /** 125 * @param {?Event} event 126 */ 127 _onBoxMouseDown: function(event) 128 { 129 if (this._hideTimeoutId) { 130 window.clearTimeout(this._hideTimeoutId); 131 delete this._hideTimeoutId; 132 } 133 event.preventDefault(); 134 }, 135 136 _maybeHide: function() 137 { 138 if (!this._hideTimeoutId) 139 this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0); 140 }, 141 142 _show: function() 143 { 144 if (this.visible()) 145 return; 146 this._overlay = new WebInspector.SuggestBox.Overlay(); 147 this._bodyElement.addEventListener("mousedown", this._maybeHideBound, true); 148 149 this._leftSpacerElement = this._overlay.element.createChild("div", "suggest-box-left-spacer"); 150 this._horizontalElement = this._overlay.element.createChild("div", "suggest-box-horizontal"); 151 this._topSpacerElement = this._horizontalElement.createChild("div", "suggest-box-top-spacer"); 152 this._horizontalElement.appendChild(this._element); 153 this._bottomSpacerElement = this._horizontalElement.createChild("div", "suggest-box-bottom-spacer"); 154 }, 155 156 hide: function() 157 { 158 if (!this.visible()) 159 return; 160 161 this._bodyElement.removeEventListener("mousedown", this._maybeHideBound, true); 162 this._element.remove(); 163 this._overlay.dispose(); 164 delete this._overlay; 165 delete this._selectedElement; 166 this._selectedIndex = -1; 167 delete this._lastAnchorBox; 168 }, 169 170 removeFromElement: function() 171 { 172 this.hide(); 173 }, 174 175 /** 176 * @param {boolean=} isIntermediateSuggestion 177 */ 178 _applySuggestion: function(isIntermediateSuggestion) 179 { 180 if (!this.visible() || !this._selectedElement) 181 return false; 182 183 var suggestion = this._selectedElement.textContent; 184 if (!suggestion) 185 return false; 186 187 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion); 188 return true; 189 }, 190 191 /** 192 * @return {boolean} 193 */ 194 acceptSuggestion: function() 195 { 196 var result = this._applySuggestion(); 197 this.hide(); 198 if (!result) 199 return false; 200 201 this._suggestBoxDelegate.acceptSuggestion(); 202 203 return true; 204 }, 205 206 /** 207 * @param {number} shift 208 * @param {boolean=} isCircular 209 * @return {boolean} is changed 210 */ 211 _selectClosest: function(shift, isCircular) 212 { 213 if (!this._length) 214 return false; 215 216 if (this._selectedIndex === -1 && shift < 0) 217 shift += 1; 218 219 var index = this._selectedIndex + shift; 220 221 if (isCircular) 222 index = (this._length + index) % this._length; 223 else 224 index = Number.constrain(index, 0, this._length - 1); 225 226 this._selectItem(index, true); 227 this._applySuggestion(true); 228 return true; 229 }, 230 231 /** 232 * @param {?Event} event 233 */ 234 _onItemMouseDown: function(event) 235 { 236 this._selectedElement = event.currentTarget; 237 this.acceptSuggestion(); 238 event.consume(true); 239 }, 240 241 /** 242 * @param {string} prefix 243 * @param {string} text 244 */ 245 _createItemElement: function(prefix, text) 246 { 247 var element = document.createElement("div"); 248 element.className = "suggest-box-content-item source-code"; 249 element.tabIndex = -1; 250 if (prefix && prefix.length && !text.indexOf(prefix)) { 251 var prefixElement = element.createChild("span", "prefix"); 252 prefixElement.textContent = prefix; 253 var suffixElement = element.createChild("span", "suffix"); 254 suffixElement.textContent = text.substring(prefix.length); 255 } else { 256 var suffixElement = element.createChild("span", "suffix"); 257 suffixElement.textContent = text; 258 } 259 element.createChild("span", "spacer"); 260 element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false); 261 return element; 262 }, 263 264 /** 265 * @param {!Array.<string>} items 266 * @param {string} userEnteredText 267 */ 268 _updateItems: function(items, userEnteredText) 269 { 270 this._length = items.length; 271 this._element.removeChildren(); 272 delete this._selectedElement; 273 274 for (var i = 0; i < items.length; ++i) { 275 var item = items[i]; 276 var currentItemElement = this._createItemElement(userEnteredText, item); 277 this._element.appendChild(currentItemElement); 278 } 279 }, 280 281 /** 282 * @param {number} index 283 * @param {boolean} scrollIntoView 284 */ 285 _selectItem: function(index, scrollIntoView) 286 { 287 if (this._selectedElement) 288 this._selectedElement.classList.remove("selected"); 289 290 this._selectedIndex = index; 291 if (index < 0) 292 return; 293 294 this._selectedElement = this._element.children[index]; 295 this._selectedElement.classList.add("selected"); 296 297 if (scrollIntoView) 298 this._selectedElement.scrollIntoViewIfNeeded(false); 299 }, 300 301 /** 302 * @param {!Array.<string>} completions 303 * @param {boolean} canShowForSingleItem 304 * @param {string} userEnteredText 305 */ 306 _canShowBox: function(completions, canShowForSingleItem, userEnteredText) 307 { 308 if (!completions || !completions.length) 309 return false; 310 311 if (completions.length > 1) 312 return true; 313 314 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes. 315 return canShowForSingleItem && completions[0] !== userEnteredText; 316 }, 317 318 _ensureRowCountPerViewport: function() 319 { 320 if (this._rowCountPerViewport) 321 return; 322 if (!this._element.firstChild) 323 return; 324 325 this._rowCountPerViewport = Math.floor(this._element.offsetHeight / this._element.firstChild.offsetHeight); 326 }, 327 328 /** 329 * @param {!AnchorBox} anchorBox 330 * @param {!Array.<string>} completions 331 * @param {number} selectedIndex 332 * @param {boolean} canShowForSingleItem 333 * @param {string} userEnteredText 334 */ 335 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText) 336 { 337 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) { 338 this._updateItems(completions, userEnteredText); 339 this._show(); 340 this._updateBoxPosition(anchorBox); 341 this._selectItem(selectedIndex, selectedIndex > 0); 342 delete this._rowCountPerViewport; 343 } else 344 this.hide(); 345 }, 346 347 /** 348 * @param {!KeyboardEvent} event 349 * @return {boolean} 350 */ 351 keyPressed: function(event) 352 { 353 switch (event.keyIdentifier) { 354 case "Up": 355 return this.upKeyPressed(); 356 case "Down": 357 return this.downKeyPressed(); 358 case "PageUp": 359 return this.pageUpKeyPressed(); 360 case "PageDown": 361 return this.pageDownKeyPressed(); 362 case "Enter": 363 return this.enterKeyPressed(); 364 } 365 return false; 366 }, 367 368 /** 369 * @return {boolean} 370 */ 371 upKeyPressed: function() 372 { 373 return this._selectClosest(-1, true); 374 }, 375 376 /** 377 * @return {boolean} 378 */ 379 downKeyPressed: function() 380 { 381 return this._selectClosest(1, true); 382 }, 383 384 /** 385 * @return {boolean} 386 */ 387 pageUpKeyPressed: function() 388 { 389 this._ensureRowCountPerViewport(); 390 return this._selectClosest(-this._rowCountPerViewport, false); 391 }, 392 393 /** 394 * @return {boolean} 395 */ 396 pageDownKeyPressed: function() 397 { 398 this._ensureRowCountPerViewport(); 399 return this._selectClosest(this._rowCountPerViewport, false); 400 }, 401 402 /** 403 * @return {boolean} 404 */ 405 enterKeyPressed: function() 406 { 407 var hasSelectedItem = !!this._selectedElement; 408 this.acceptSuggestion(); 409 410 // Report the event as non-handled if there is no selected item, 411 // to commit the input or handle it otherwise. 412 return hasSelectedItem; 413 } 414} 415 416/** 417 * @constructor 418 */ 419WebInspector.SuggestBox.Overlay = function() 420{ 421 this.element = document.createElement("div"); 422 this.element.classList.add("suggest-box-overlay"); 423 this._resize(); 424 document.body.appendChild(this.element); 425} 426 427WebInspector.SuggestBox.Overlay.prototype = { 428 _resize: function() 429 { 430 var container = WebInspector.Dialog.modalHostView().element; 431 var containerBox = container.boxInWindow(container.ownerDocument.defaultView); 432 433 this.element.style.left = containerBox.x + "px"; 434 this.element.style.top = containerBox.y + "px"; 435 this.element.style.height = containerBox.height + "px"; 436 this.element.style.width = containerBox.width + "px"; 437 }, 438 439 dispose: function() 440 { 441 this.element.remove(); 442 } 443} 444