UNPKG

chrome-devtools-frontend

Version:
586 lines (519 loc) • 16.6 kB
/* * Copyright (C) 2013 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as ARIAUtils from './ARIAUtils.js'; import {Icon} from './Icon.js'; import {KeyboardShortcut, Modifiers} from './KeyboardShortcut.js'; import {bindCheckbox} from './SettingsUI.js'; import {Suggestions} from './SuggestBox.js'; // eslint-disable-line no-unused-vars import {Events, TextPrompt} from './TextPrompt.js'; import {ToolbarButton, ToolbarSettingToggle} from './Toolbar.js'; // eslint-disable-line no-unused-vars import {Tooltip} from './Tooltip.js'; import {CheckboxLabel, createTextChild} from './UIUtils.js'; import {HBox} from './Widget.js'; export class FilterBar extends HBox { /** * @param {string} name * @param {boolean=} visibleByDefault */ constructor(name, visibleByDefault) { super(); this.registerRequiredCSS('ui/filter.css', {enableLegacyPatching: true}); this._enabled = true; this.element.classList.add('filter-bar'); this._stateSetting = Common.Settings.Settings.instance().createSetting('filterBar-' + name + '-toggled', Boolean(visibleByDefault)); this._filterButton = new ToolbarSettingToggle(this._stateSetting, 'largeicon-filter', Common.UIString.UIString('Filter')); /** @type {!Array<!FilterUI>} */ this._filters = []; this._updateFilterBar(); this._stateSetting.addChangeListener(this._updateFilterBar.bind(this)); } /** * @return {!ToolbarButton} */ filterButton() { return this._filterButton; } /** * @param {!FilterUI} filter */ addFilter(filter) { this._filters.push(filter); this.element.appendChild(filter.element()); filter.addEventListener(FilterUI.Events.FilterChanged, this._filterChanged, this); this._updateFilterButton(); } /** @param {boolean} enabled */ setEnabled(enabled) { this._enabled = enabled; this._filterButton.setEnabled(enabled); this._updateFilterBar(); } forceShowFilterBar() { this._alwaysShowFilters = true; this._updateFilterBar(); } showOnce() { this._stateSetting.set(true); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _filterChanged(event) { this._updateFilterButton(); this.dispatchEventToListeners(FilterBar.Events.Changed); } /** * @override */ wasShown() { super.wasShown(); this._updateFilterBar(); } _updateFilterBar() { if (!this.parentWidget() || this._showingWidget) { return; } if (this.visible()) { this._showingWidget = true; this.showWidget(); this._showingWidget = false; } else { this.hideWidget(); } } /** * @override */ focus() { for (let i = 0; i < this._filters.length; ++i) { if (this._filters[i] instanceof TextFilterUI) { const textFilterUI = /** @type {!TextFilterUI} */ (this._filters[i]); textFilterUI.focus(); break; } } } _updateFilterButton() { let isActive = false; for (const filter of this._filters) { isActive = isActive || filter.isActive(); } this._filterButton.setDefaultWithRedColor(isActive); this._filterButton.setToggleWithRedColor(isActive); } clear() { this.element.removeChildren(); this._filters = []; this._updateFilterButton(); } setting() { return this._stateSetting; } visible() { return this._alwaysShowFilters || (this._stateSetting.get() && this._enabled); } } FilterBar.Events = { Changed: Symbol('Changed'), }; /** * @interface */ export class FilterUI extends Common.EventTarget.EventTarget { /** * @return {boolean} */ isActive() { throw new Error('not implemented'); } /** * @return {!Element} */ element() { throw new Error('not implemented'); } } /** @enum {symbol} */ FilterUI.Events = { FilterChanged: Symbol('FilterChanged') }; /** * @implements {FilterUI} */ export class TextFilterUI extends Common.ObjectWrapper.ObjectWrapper { constructor() { super(); this._filterElement = document.createElement('div'); this._filterElement.className = 'filter-text-filter'; const container = this._filterElement.createChild('div', 'filter-input-container'); this._filterInputElement = container.createChild('span', 'filter-input-field'); this._prompt = new TextPrompt(); this._prompt.initialize(this._completions.bind(this), ' ', true); /** @type {!HTMLElement} */ this._proxyElement = /** @type {!HTMLElement} */ (this._prompt.attach(this._filterInputElement)); Tooltip.install(this._proxyElement, Common.UIString.UIString('e.g. /small[\\d]+/ url:a.com/b')); this._prompt.setPlaceholder(Common.UIString.UIString('Filter')); this._prompt.addEventListener(Events.TextChanged, this._valueChanged.bind(this)); /** @type {?function(string, string, boolean=):!Promise<!Suggestions>} */ this._suggestionProvider = null; const clearButton = container.createChild('div', 'filter-input-clear-button'); clearButton.appendChild(Icon.create('mediumicon-gray-cross-hover', 'filter-cancel-button')); clearButton.addEventListener('click', () => { this.clear(); this.focus(); }); this._updateEmptyStyles(); } /** * @param {string} expression * @param {string} prefix * @param {boolean=} force * @return {!Promise<!Suggestions>} */ _completions(expression, prefix, force) { if (this._suggestionProvider) { return this._suggestionProvider(expression, prefix, force); } return Promise.resolve([]); } /** * @override * @return {boolean} */ isActive() { return Boolean(this._prompt.text()); } /** * @override * @return {!Element} */ element() { return this._filterElement; } /** * @return {string} */ value() { return this._prompt.textWithCurrentSuggestion(); } /** * @param {string} value */ setValue(value) { this._prompt.setText(value); this._valueChanged(); } focus() { this._filterInputElement.focus(); } /** * @param {(function(string, string, boolean=):!Promise<!Suggestions>)} suggestionProvider */ setSuggestionProvider(suggestionProvider) { this._prompt.clearAutocomplete(); this._suggestionProvider = suggestionProvider; } _valueChanged() { this.dispatchEventToListeners(FilterUI.Events.FilterChanged, null); this._updateEmptyStyles(); } _updateEmptyStyles() { this._filterElement.classList.toggle('filter-text-empty', !this._prompt.text()); } clear() { this.setValue(''); } } /** * @implements {FilterUI} */ export class NamedBitSetFilterUI extends Common.ObjectWrapper.ObjectWrapper { /** * @param {!Array.<!Item>} items * @param {!Common.Settings.Setting<*>=} setting */ constructor(items, setting) { super(); this._filtersElement = document.createElement('div'); this._filtersElement.classList.add('filter-bitset-filter'); ARIAUtils.markAsListBox(this._filtersElement); ARIAUtils.markAsMultiSelectable(this._filtersElement); Tooltip.install( this._filtersElement, Common.UIString.UIString( '%sClick to select multiple types', KeyboardShortcut.shortcutToString('', Modifiers.CtrlOrMeta))); /** @type {!WeakMap<!HTMLElement, string>} */ this._typeFilterElementTypeNames = new WeakMap(); /** @type {!Set<string>} */ this._allowedTypes = new Set(); /** @type {!Array.<!HTMLElement>} */ this._typeFilterElements = []; this._addBit(NamedBitSetFilterUI.ALL_TYPES, Common.UIString.UIString('All')); this._typeFilterElements[0].tabIndex = 0; this._filtersElement.createChild('div', 'filter-bitset-filter-divider'); for (let i = 0; i < items.length; ++i) { this._addBit(items[i].name, items[i].label(), items[i].title); } if (setting) { this._setting = setting; setting.addChangeListener(this._settingChanged.bind(this)); this._settingChanged(); } else { this._toggleTypeFilter(NamedBitSetFilterUI.ALL_TYPES, false /* allowMultiSelect */); } } reset() { this._toggleTypeFilter(NamedBitSetFilterUI.ALL_TYPES, false /* allowMultiSelect */); } /** * @override * @return {boolean} */ isActive() { return !this._allowedTypes.has(NamedBitSetFilterUI.ALL_TYPES); } /** * @override * @return {!Element} */ element() { return this._filtersElement; } /** * @param {string} typeName * @return {boolean} */ accept(typeName) { return this._allowedTypes.has(NamedBitSetFilterUI.ALL_TYPES) || this._allowedTypes.has(typeName); } _settingChanged() { const allowedTypesFromSetting = /** @type {!Common.Settings.Setting<*>} */ (this._setting).get(); this._allowedTypes = new Set(); for (const element of this._typeFilterElements) { const typeName = this._typeFilterElementTypeNames.get(element); if (typeName && allowedTypesFromSetting[typeName]) { this._allowedTypes.add(typeName); } } this._update(); } _update() { if (this._allowedTypes.size === 0 || this._allowedTypes.has(NamedBitSetFilterUI.ALL_TYPES)) { this._allowedTypes = new Set(); this._allowedTypes.add(NamedBitSetFilterUI.ALL_TYPES); } for (const element of this._typeFilterElements) { const typeName = this._typeFilterElementTypeNames.get(element); const active = this._allowedTypes.has(typeName || ''); element.classList.toggle('selected', active); ARIAUtils.setSelected(element, active); } this.dispatchEventToListeners(FilterUI.Events.FilterChanged, null); } /** * @param {string} name * @param {string} label * @param {string=} title */ _addBit(name, label, title) { const typeFilterElement = /** @type {!HTMLElement} */ (this._filtersElement.createChild('span', name)); typeFilterElement.tabIndex = -1; this._typeFilterElementTypeNames.set(typeFilterElement, name); createTextChild(typeFilterElement, label); ARIAUtils.markAsOption(typeFilterElement); if (title) { typeFilterElement.title = title; } typeFilterElement.addEventListener('click', this._onTypeFilterClicked.bind(this), false); typeFilterElement.addEventListener('keydown', this._onTypeFilterKeydown.bind(this), false); this._typeFilterElements.push(typeFilterElement); } /** * @param {!Event} event */ _onTypeFilterClicked(event) { const e = /** @type {!KeyboardEvent} */ (event); let toggle; if (Host.Platform.isMac()) { toggle = e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey; } else { toggle = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey; } if (e.target) { const element = /** @type {!HTMLElement} */ (e.target); const typeName = /** @type {string} */ (this._typeFilterElementTypeNames.get(element)); this._toggleTypeFilter(typeName, toggle); } } /** * @param {!Event} ev */ _onTypeFilterKeydown(ev) { const event = /** @type {!KeyboardEvent} */ (ev); const element = /** @type {?HTMLElement} */ (event.target); if (!element) { return; } if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { if (this._keyFocusNextBit(element, true /* selectPrevious */)) { event.consume(true); } } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { if (this._keyFocusNextBit(element, false /* selectPrevious */)) { event.consume(true); } } else if (isEnterOrSpaceKey(event)) { this._onTypeFilterClicked(event); } } /** * @param {!HTMLElement} target * @param {boolean} selectPrevious * @returns {!boolean} */ _keyFocusNextBit(target, selectPrevious) { const index = this._typeFilterElements.indexOf(target); if (index === -1) { return false; } const nextIndex = selectPrevious ? index - 1 : index + 1; if (nextIndex < 0 || nextIndex >= this._typeFilterElements.length) { return false; } const nextElement = this._typeFilterElements[nextIndex]; nextElement.tabIndex = 0; target.tabIndex = -1; nextElement.focus(); return true; } /** * @param {string} typeName * @param {boolean} allowMultiSelect */ _toggleTypeFilter(typeName, allowMultiSelect) { if (allowMultiSelect && typeName !== NamedBitSetFilterUI.ALL_TYPES) { this._allowedTypes.delete(NamedBitSetFilterUI.ALL_TYPES); } else { this._allowedTypes = new Set(); } if (this._allowedTypes.has(typeName)) { this._allowedTypes.delete(typeName); } else { this._allowedTypes.add(typeName); } if (this._setting) { // Settings do not support `Sets` so convert it back to the Map-like object. const updatedSetting = /** @type {*} */ ({}); for (const type of this._allowedTypes) { updatedSetting[type] = true; } this._setting.set(updatedSetting); } else { this._update(); } } } NamedBitSetFilterUI.ALL_TYPES = 'all'; /** * @implements {FilterUI} */ export class CheckboxFilterUI extends Common.ObjectWrapper.ObjectWrapper { /** * @param {string} className * @param {string} title * @param {boolean=} activeWhenChecked * @param {!Common.Settings.Setting<boolean>=} setting */ constructor(className, title, activeWhenChecked, setting) { super(); this._filterElement = /** @type {!HTMLDivElement} */ (document.createElement('div')); this._filterElement.classList.add('filter-checkbox-filter'); this._activeWhenChecked = Boolean(activeWhenChecked); this._label = CheckboxLabel.create(title); this._filterElement.appendChild(this._label); this._checkboxElement = this._label.checkboxElement; if (setting) { bindCheckbox(this._checkboxElement, setting); } else { this._checkboxElement.checked = true; } this._checkboxElement.addEventListener('change', this._fireUpdated.bind(this), false); } /** * @override * @return {boolean} */ isActive() { return this._activeWhenChecked === this._checkboxElement.checked; } /** * @return {boolean} */ checked() { return this._checkboxElement.checked; } /** * @param {boolean} checked */ setChecked(checked) { this._checkboxElement.checked = checked; } /** * @override * @return {!HTMLDivElement} */ element() { return this._filterElement; } /** * @return {!Element} */ labelElement() { return this._label; } _fireUpdated() { this.dispatchEventToListeners(FilterUI.Events.FilterChanged, null); } /** * @param {string} backgroundColor * @param {string} borderColor */ setColor(backgroundColor, borderColor) { this._label.backgroundColor = backgroundColor; this._label.borderColor = borderColor; } } /** @typedef {{name: string, label: function():string, title: (string|undefined)}} */ // @ts-ignore typedef export let Item;