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 * @constructor 33 * @extends {WebInspector.Object} 34 */ 35 WebInspector.FilterBar = function() 36 { 37 this._filtersShown = false; 38 this._element = document.createElement("div"); 39 this._element.className = "hbox"; 40 41 this._filterButton = new WebInspector.StatusBarButton(WebInspector.UIString("Filter"), "filters-toggle", 3); 42 this._filterButton.element.addEventListener("click", this._handleFilterButtonClick.bind(this), false); 43 44 this._filters = []; 45 } 46 47 WebInspector.FilterBar.Events = { 48 FiltersToggled: "FiltersToggled" 49 } 50 51 WebInspector.FilterBar.FilterBarState = { 52 Inactive : "inactive", 53 Active : "active", 54 Shown : "shown" 55 }; 56 57 WebInspector.FilterBar.prototype = { 58 /** 59 * @param {string} name 60 */ 61 setName: function(name) 62 { 63 this._stateSetting = WebInspector.settings.createSetting("filterBar-" + name + "-toggled", false); 64 this._setState(this._stateSetting.get()); 65 }, 66 67 /** 68 * @return {!WebInspector.StatusBarButton} 69 */ 70 filterButton: function() 71 { 72 return this._filterButton; 73 }, 74 75 /** 76 * @return {!Element} 77 */ 78 filtersElement: function() 79 { 80 return this._element; 81 }, 82 83 /** 84 * @return {boolean} 85 */ 86 filtersToggled: function() 87 { 88 return this._filtersShown; 89 }, 90 91 /** 92 * @param {!WebInspector.FilterUI} filter 93 */ 94 addFilter: function(filter) 95 { 96 this._filters.push(filter); 97 this._element.appendChild(filter.element()); 98 filter.addEventListener(WebInspector.FilterUI.Events.FilterChanged, this._filterChanged, this); 99 this._updateFilterButton(); 100 }, 101 102 /** 103 * @param {!WebInspector.Event} event 104 */ 105 _filterChanged: function(event) 106 { 107 this._updateFilterButton(); 108 }, 109 110 /** 111 * @return {string} 112 */ 113 _filterBarState: function() 114 { 115 if (this._filtersShown) 116 return WebInspector.FilterBar.FilterBarState.Shown; 117 var isActive = false; 118 for (var i = 0; i < this._filters.length; ++i) { 119 if (this._filters[i].isActive()) 120 return WebInspector.FilterBar.FilterBarState.Active; 121 } 122 return WebInspector.FilterBar.FilterBarState.Inactive; 123 }, 124 125 _updateFilterButton: function() 126 { 127 this._filterButton.state = this._filterBarState(); 128 }, 129 130 /** 131 * @param {!Event} event 132 */ 133 _handleFilterButtonClick: function(event) 134 { 135 this._setState(!this._filtersShown); 136 }, 137 138 /** 139 * @param {boolean} filtersShown 140 */ 141 _setState: function(filtersShown) 142 { 143 if (this._filtersShown === filtersShown) 144 return; 145 146 this._filtersShown = filtersShown; 147 if (this._stateSetting) 148 this._stateSetting.set(filtersShown); 149 150 this._updateFilterButton(); 151 this.dispatchEventToListeners(WebInspector.FilterBar.Events.FiltersToggled, this._filtersShown); 152 if (this._filtersShown) { 153 for (var i = 0; i < this._filters.length; ++i) { 154 if (this._filters[i] instanceof WebInspector.TextFilterUI) { 155 var textFilterUI = /** @type {!WebInspector.TextFilterUI} */ (this._filters[i]); 156 textFilterUI.focus(); 157 } 158 } 159 } 160 }, 161 162 clear: function() 163 { 164 this._element.removeChildren(); 165 this._filters = []; 166 this._updateFilterButton(); 167 }, 168 169 __proto__: WebInspector.Object.prototype 170 } 171 172 /** 173 * @interface 174 * @extends {WebInspector.EventTarget} 175 */ 176 WebInspector.FilterUI = function() 177 { 178 } 179 180 WebInspector.FilterUI.Events = { 181 FilterChanged: "FilterChanged" 182 } 183 184 WebInspector.FilterUI.prototype = { 185 /** 186 * @return {boolean} 187 */ 188 isActive: function() { }, 189 190 /** 191 * @return {!Element} 192 */ 193 element: function() { } 194 } 195 196 /** 197 * @constructor 198 * @extends {WebInspector.Object} 199 * @implements {WebInspector.FilterUI} 200 * @implements {WebInspector.SuggestBoxDelegate} 201 * @param {boolean=} supportRegex 202 */ 203 WebInspector.TextFilterUI = function(supportRegex) 204 { 205 this._supportRegex = !!supportRegex; 206 this._regex = null; 207 208 this._filterElement = document.createElement("div"); 209 this._filterElement.className = "filter-text-filter"; 210 211 this._filterInputElement = /** @type {!HTMLInputElement} */ (this._filterElement.createChild("input", "search-replace toolbar-replace-control")); 212 this._filterInputElement.placeholder = WebInspector.UIString("Filter"); 213 this._filterInputElement.id = "filter-input-field"; 214 this._filterInputElement.addEventListener("mousedown", this._onFilterFieldManualFocus.bind(this), false); // when the search field is manually selected 215 this._filterInputElement.addEventListener("input", this._onInput.bind(this), false); 216 this._filterInputElement.addEventListener("change", this._onChange.bind(this), false); 217 this._filterInputElement.addEventListener("keydown", this._onInputKeyDown.bind(this), true); 218 this._filterInputElement.addEventListener("blur", this._onBlur.bind(this), true); 219 220 /** @type {?WebInspector.TextFilterUI.SuggestionBuilder} */ 221 this._suggestionBuilder = null; 222 223 this._suggestBox = new WebInspector.SuggestBox(this); 224 225 if (this._supportRegex) { 226 this._filterElement.classList.add("supports-regex"); 227 this._regexCheckBox = this._filterElement.createChild("input"); 228 this._regexCheckBox.type = "checkbox"; 229 this._regexCheckBox.id = "text-filter-regex"; 230 this._regexCheckBox.addEventListener("change", this._onInput.bind(this), false); 231 232 this._regexLabel = this._filterElement.createChild("label"); 233 this._regexLabel.htmlFor = "text-filter-regex"; 234 this._regexLabel.textContent = WebInspector.UIString("Regex"); 235 } 236 } 237 238 WebInspector.TextFilterUI.prototype = { 239 /** 240 * @return {boolean} 241 */ 242 isActive: function() 243 { 244 return !!this._filterInputElement.value; 245 }, 246 247 /** 248 * @return {!Element} 249 */ 250 element: function() 251 { 252 return this._filterElement; 253 }, 254 255 /** 256 * @return {string} 257 */ 258 value: function() 259 { 260 return this._filterInputElement.value; 261 }, 262 263 /** 264 * @param {string} value 265 */ 266 setValue: function(value) 267 { 268 this._filterInputElement.value = value; 269 this._valueChanged(false); 270 }, 271 272 /** 273 * @return {?RegExp} 274 */ 275 regex: function() 276 { 277 return this._regex; 278 }, 279 280 /** 281 * @param {!Event} event 282 */ 283 _onFilterFieldManualFocus: function(event) 284 { 285 WebInspector.setCurrentFocusElement(event.target); 286 }, 287 288 /** 289 * @param {!Event} event 290 */ 291 _onBlur: function(event) 292 { 293 this._cancelSuggestion(); 294 }, 295 296 _cancelSuggestion: function() 297 { 298 if (this._suggestionBuilder && this._suggestBox.visible) { 299 this._suggestionBuilder.unapplySuggestion(this._filterInputElement); 300 this._suggestBox.hide(); 301 } 302 }, 303 304 _onInput: function() 305 { 306 this._valueChanged(true); 307 }, 308 309 _onChange: function() 310 { 311 this._valueChanged(false); 312 }, 313 314 focus: function() 315 { 316 this._filterInputElement.focus(); 317 }, 318 319 /** 320 * @param {?WebInspector.TextFilterUI.SuggestionBuilder} suggestionBuilder 321 */ 322 setSuggestionBuilder: function(suggestionBuilder) 323 { 324 this._cancelSuggestion(); 325 this._suggestionBuilder = suggestionBuilder; 326 }, 327 328 _updateSuggestions: function() 329 { 330 if (!this._suggestionBuilder) 331 return; 332 var suggestions = this._suggestionBuilder.buildSuggestions(this._filterInputElement); 333 if (suggestions && suggestions.length) { 334 if (this._suppressSuggestion) 335 delete this._suppressSuggestion; 336 else 337 this._suggestionBuilder.applySuggestion(this._filterInputElement, suggestions[0], true); 338 var anchorBox = this._filterInputElement.boxInWindow().relativeTo(new AnchorBox(-3, 0)); 339 this._suggestBox.updateSuggestions(anchorBox, suggestions, 0, true, ""); 340 } else { 341 this._suggestBox.hide(); 342 } 343 }, 344 345 /** 346 * @param {boolean} showSuggestions 347 */ 348 _valueChanged: function(showSuggestions) 349 { 350 if (showSuggestions) 351 this._updateSuggestions(); 352 else 353 this._suggestBox.hide(); 354 355 var filterQuery = this.value(); 356 357 this._regex = null; 358 this._filterInputElement.classList.remove("filter-text-invalid"); 359 if (filterQuery) { 360 if (this._supportRegex && this._regexCheckBox.checked) { 361 try { 362 this._regex = new RegExp(filterQuery, "i"); 363 } catch (e) { 364 this._filterInputElement.classList.add("filter-text-invalid"); 365 } 366 } else { 367 this._regex = createPlainTextSearchRegex(filterQuery, "i"); 368 } 369 } 370 371 this._dispatchFilterChanged(); 372 }, 373 374 _dispatchFilterChanged: function() 375 { 376 this.dispatchEventToListeners(WebInspector.FilterUI.Events.FilterChanged, null); 377 }, 378 379 /** 380 * @param {!Event} event 381 * @return {boolean} 382 */ 383 _onInputKeyDown: function(event) 384 { 385 var handled = false; 386 if (event.keyIdentifier === "U+0008") { // Backspace 387 this._suppressSuggestion = true; 388 } else if (this._suggestBox.visible()) { 389 if (event.keyIdentifier === "U+001B") { // Esc 390 this._cancelSuggestion(); 391 handled = true; 392 } else if (event.keyIdentifier === "U+0009") { // Tab 393 this._suggestBox.acceptSuggestion(); 394 this._valueChanged(true); 395 handled = true; 396 } else { 397 handled = this._suggestBox.keyPressed(/** @type {!KeyboardEvent} */ (event)); 398 } 399 } 400 if (handled) 401 event.consume(true); 402 return handled; 403 }, 404 405 /** 406 * @override 407 * @param {string} suggestion 408 * @param {boolean=} isIntermediateSuggestion 409 */ 410 applySuggestion: function(suggestion, isIntermediateSuggestion) 411 { 412 if (!this._suggestionBuilder) 413 return; 414 this._suggestionBuilder.applySuggestion(this._filterInputElement, suggestion, !!isIntermediateSuggestion); 415 if (isIntermediateSuggestion) 416 this._dispatchFilterChanged(); 417 }, 418 419 /** @override */ 420 acceptSuggestion: function() 421 { 422 this._filterInputElement.scrollLeft = this._filterInputElement.scrollWidth; 423 this._valueChanged(true); 424 }, 425 426 __proto__: WebInspector.Object.prototype 427 } 428 429 /** 430 * @interface 431 */ 432 WebInspector.TextFilterUI.SuggestionBuilder = function() 433 { 434 } 435 436 WebInspector.TextFilterUI.SuggestionBuilder.prototype = { 437 /** 438 * @param {!HTMLInputElement} input 439 * @return {?Array.<string>} 440 */ 441 buildSuggestions: function(input) { }, 442 443 /** 444 * @param {!HTMLInputElement} input 445 * @param {string} suggestion 446 * @param {boolean} isIntermediate 447 */ 448 applySuggestion: function(input, suggestion, isIntermediate) { }, 449 450 /** 451 * @param {!HTMLInputElement} input 452 */ 453 unapplySuggestion: function(input) { } 454 } 455 456 /** 457 * @constructor 458 * @extends {WebInspector.Object} 459 * @implements {WebInspector.FilterUI} 460 * @param {!Array.<!WebInspector.NamedBitSetFilterUI.Item>} items 461 * @param {!WebInspector.Setting=} setting 462 */ 463 WebInspector.NamedBitSetFilterUI = function(items, setting) 464 { 465 this._filtersElement = document.createElement("div"); 466 this._filtersElement.className = "filter-bitset-filter status-bar-item"; 467 this._filtersElement.title = WebInspector.UIString("Use %s Click to select multiple types.", WebInspector.KeyboardShortcut.shortcutToString("", WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta)); 468 469 this._allowedTypes = {}; 470 this._typeFilterElements = {}; 471 this._addBit(WebInspector.NamedBitSetFilterUI.ALL_TYPES, WebInspector.UIString("All")); 472 this._filtersElement.createChild("div", "filter-bitset-filter-divider"); 473 474 for (var i = 0; i < items.length; ++i) 475 this._addBit(items[i].name, items[i].label); 476 477 if (setting) { 478 this._setting = setting; 479 setting.addChangeListener(this._settingChanged.bind(this)); 480 this._settingChanged(); 481 } else { 482 this._toggleTypeFilter(WebInspector.NamedBitSetFilterUI.ALL_TYPES, false); 483 } 484 } 485 486 /** @typedef {{name: string, label: string}} */ 487 WebInspector.NamedBitSetFilterUI.Item; 488 489 WebInspector.NamedBitSetFilterUI.ALL_TYPES = "all"; 490 491 WebInspector.NamedBitSetFilterUI.prototype = { 492 /** 493 * @return {boolean} 494 */ 495 isActive: function() 496 { 497 return !this._allowedTypes[WebInspector.NamedBitSetFilterUI.ALL_TYPES]; 498 }, 499 500 /** 501 * @return {!Element} 502 */ 503 element: function() 504 { 505 return this._filtersElement; 506 }, 507 508 /** 509 * @param {string} typeName 510 * @return {boolean} 511 */ 512 accept: function(typeName) 513 { 514 return !!this._allowedTypes[WebInspector.NamedBitSetFilterUI.ALL_TYPES] || !!this._allowedTypes[typeName]; 515 }, 516 517 _settingChanged: function() 518 { 519 var allowedTypes = this._setting.get(); 520 this._allowedTypes = {}; 521 for (var typeName in this._typeFilterElements) { 522 if (allowedTypes[typeName]) 523 this._allowedTypes[typeName] = true; 524 } 525 this._update(); 526 }, 527 528 _update: function() 529 { 530 if ((Object.keys(this._allowedTypes).length === 0) || this._allowedTypes[WebInspector.NamedBitSetFilterUI.ALL_TYPES]) { 531 this._allowedTypes = {}; 532 this._allowedTypes[WebInspector.NamedBitSetFilterUI.ALL_TYPES] = true; 533 } 534 for (var typeName in this._typeFilterElements) 535 this._typeFilterElements[typeName].classList.toggle("selected", this._allowedTypes[typeName]); 536 this.dispatchEventToListeners(WebInspector.FilterUI.Events.FilterChanged, null); 537 }, 538 539 /** 540 * @param {string} name 541 * @param {string} label 542 */ 543 _addBit: function(name, label) 544 { 545 var typeFilterElement = this._filtersElement.createChild("li", name); 546 typeFilterElement.typeName = name; 547 typeFilterElement.createTextChild(label); 548 typeFilterElement.addEventListener("click", this._onTypeFilterClicked.bind(this), false); 549 this._typeFilterElements[name] = typeFilterElement; 550 }, 551 552 /** 553 * @param {!Event} e 554 */ 555 _onTypeFilterClicked: function(e) 556 { 557 var toggle; 558 if (WebInspector.isMac()) 559 toggle = e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey; 560 else 561 toggle = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey; 562 this._toggleTypeFilter(e.target.typeName, toggle); 563 }, 564 565 /** 566 * @param {string} typeName 567 * @param {boolean} allowMultiSelect 568 */ 569 _toggleTypeFilter: function(typeName, allowMultiSelect) 570 { 571 if (allowMultiSelect && typeName !== WebInspector.NamedBitSetFilterUI.ALL_TYPES) 572 this._allowedTypes[WebInspector.NamedBitSetFilterUI.ALL_TYPES] = false; 573 else 574 this._allowedTypes = {}; 575 576 this._allowedTypes[typeName] = !this._allowedTypes[typeName]; 577 578 if (this._setting) 579 this._setting.set(this._allowedTypes); 580 else 581 this._update(); 582 }, 583 584 __proto__: WebInspector.Object.prototype 585 } 586 587 /** 588 * @constructor 589 * @implements {WebInspector.FilterUI} 590 * @extends {WebInspector.Object} 591 * @param {!Array.<!{value: *, label: string, title: string}>} options 592 */ 593 WebInspector.ComboBoxFilterUI = function(options) 594 { 595 this._filterElement = document.createElement("div"); 596 this._filterElement.className = "filter-combobox-filter"; 597 598 this._options = options; 599 this._filterComboBox = new WebInspector.StatusBarComboBox(this._filterChanged.bind(this)); 600 for (var i = 0; i < options.length; ++i) { 601 var filterOption = options[i]; 602 var option = document.createElement("option"); 603 option.text = filterOption.label; 604 option.title = filterOption.title; 605 this._filterComboBox.addOption(option); 606 this._filterComboBox.element.title = this._filterComboBox.selectedOption().title; 607 } 608 this._filterElement.appendChild(this._filterComboBox.element); 609 } 610 611 WebInspector.ComboBoxFilterUI.prototype = { 612 /** 613 * @return {boolean} 614 */ 615 isActive: function() 616 { 617 return this._filterComboBox.selectedIndex() !== 0; 618 }, 619 620 /** 621 * @return {!Element} 622 */ 623 element: function() 624 { 625 return this._filterElement; 626 }, 627 628 /** 629 * @param {string} typeName 630 * @return {*} 631 */ 632 value: function(typeName) 633 { 634 var option = this._options[this._filterComboBox.selectedIndex()]; 635 return option.value; 636 }, 637 638 /** 639 * @param {number} index 640 */ 641 setSelectedIndex: function(index) 642 { 643 this._filterComboBox.setSelectedIndex(index); 644 }, 645 646 /** 647 * @return {number} 648 */ 649 selectedIndex: function(index) 650 { 651 return this._filterComboBox.selectedIndex(); 652 }, 653 654 /** 655 * @param {!Event} event 656 */ 657 _filterChanged: function(event) 658 { 659 var option = this._options[this._filterComboBox.selectedIndex()]; 660 this._filterComboBox.element.title = option.title; 661 this.dispatchEventToListeners(WebInspector.FilterUI.Events.FilterChanged, null); 662 }, 663 664 __proto__: WebInspector.Object.prototype 665 } 666 667 /** 668 * @constructor 669 * @implements {WebInspector.FilterUI} 670 * @extends {WebInspector.Object} 671 * @param {string} className 672 * @param {string} title 673 * @param {boolean=} activeWhenChecked 674 * @param {!WebInspector.Setting=} setting 675 */ 676 WebInspector.CheckboxFilterUI = function(className, title, activeWhenChecked, setting) 677 { 678 this._filterElement = document.createElement("div"); 679 this._filterElement.classList.add("filter-checkbox-filter", "filter-checkbox-filter-" + className); 680 this._activeWhenChecked = !!activeWhenChecked; 681 this._createCheckbox(title); 682 683 if (setting) { 684 this._setting = setting; 685 setting.addChangeListener(this._settingChanged.bind(this)); 686 this._settingChanged(); 687 } else { 688 this._checked = !this._activeWhenChecked; 689 this._update(); 690 } 691 } 692 693 WebInspector.CheckboxFilterUI.prototype = { 694 /** 695 * @return {boolean} 696 */ 697 isActive: function() 698 { 699 return this._activeWhenChecked === this._checked; 700 }, 701 702 /** 703 * @return {!Element} 704 */ 705 element: function() 706 { 707 return this._filterElement; 708 }, 709 710 /** 711 * @return {boolean} 712 */ 713 checked: function() 714 { 715 return this._checked; 716 }, 717 718 /** 719 * @param {boolean} state 720 */ 721 setState: function(state) 722 { 723 this._checked = state; 724 this._update(); 725 }, 726 727 _update: function() 728 { 729 this._checkElement.classList.toggle("checkbox-filter-checkbox-checked", this._checked); 730 this.dispatchEventToListeners(WebInspector.FilterUI.Events.FilterChanged, null); 731 }, 732 733 _settingChanged: function() 734 { 735 this._checked = this._setting.get(); 736 this._update(); 737 }, 738 739 /** 740 * @param {!Event} event 741 */ 742 _onClick: function(event) 743 { 744 this._checked = !this._checked; 745 if (this._setting) 746 this._setting.set(this._checked); 747 else 748 this._update(); 749 }, 750 751 /** 752 * @param {string} title 753 */ 754 _createCheckbox: function(title) 755 { 756 var label = this._filterElement.createChild("label"); 757 var checkBorder = label.createChild("div", "checkbox-filter-checkbox"); 758 this._checkElement = checkBorder.createChild("div", "checkbox-filter-checkbox-check"); 759 this._filterElement.addEventListener("click", this._onClick.bind(this), false); 760 var typeElement = label.createChild("span", "type"); 761 typeElement.textContent = title; 762 }, 763 764 __proto__: WebInspector.Object.prototype 765 } 766