[go: nahoru, domu]

blob: e7d3d73264d2b862484be4d284195214eb217af5 [file] [log] [blame]
/*
* 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 i18n from '../i18n/i18n.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 const UIStrings = {
/**
*@description Text to filter result items
*/
filter: 'Filter',
/**
*@description Text that appears when hover over the filter bar in the Network tool
*/
egSmalldUrlacomb: 'e.g. `/small[\d]+/ url:a.com/b`',
/**
*@description Text that appears when hover over the All button in the Network tool
*@example {Ctrl + } PH1
*/
sclickToSelectMultipleTypes: '{PH1}Click to select multiple types',
/**
*@description Text for everything
*/
allStrings: 'All',
};
const str_ = i18n.i18n.registerUIStrings('ui/FilterBar.js', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
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', i18nString(UIStrings.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, i18nString(UIStrings.egSmalldUrlacomb));
this._prompt.setPlaceholder(i18nString(UIStrings.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, i18nString(UIStrings.sclickToSelectMultipleTypes, {
PH1: 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, i18nString(UIStrings.allStrings));
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;