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 */
35WebInspector.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
47WebInspector.FilterBar.Events = {
48    FiltersToggled: "FiltersToggled"
49}
50
51WebInspector.FilterBar.FilterBarState = {
52    Inactive : "inactive",
53    Active : "active",
54    Shown : "shown"
55};
56
57WebInspector.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 */
176WebInspector.FilterUI = function()
177{
178}
179
180WebInspector.FilterUI.Events = {
181    FilterChanged: "FilterChanged"
182}
183
184WebInspector.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 */
203WebInspector.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
238WebInspector.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 */
432WebInspector.TextFilterUI.SuggestionBuilder = function()
433{
434}
435
436WebInspector.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 */
463WebInspector.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}} */
487WebInspector.NamedBitSetFilterUI.Item;
488
489WebInspector.NamedBitSetFilterUI.ALL_TYPES = "all";
490
491WebInspector.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 */
593WebInspector.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
611WebInspector.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 */
676WebInspector.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
693WebInspector.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