chrome-devtools-frontend
Version:
Chrome DevTools UI
409 lines (360 loc) • 15.7 kB
JavaScript
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../common/common.js';
import {ls} from '../platform/platform.js';
import * as UI from '../ui/ui.js';
import {DeviceModeModel, MaxDeviceNameLength, UA} from './DeviceModeModel.js';
import {Capability, EmulatedDevice, EmulatedDevicesList, Events, Horizontal, Vertical,} from './EmulatedDevices.js';
import {parseBrandsList, serializeBrandsList, validateAsStructuredHeadersString} from './UserAgentMetadata.js';
/** @type {!DevicesSettingsTab} */
let devicesSettingsTabInstance;
/**
* @implements {UI.ListWidget.Delegate<!EmulatedDevice>}
*/
export class DevicesSettingsTab extends UI.Widget.VBox {
/** @private */
constructor() {
super();
this.element.classList.add('settings-tab-container');
this.element.classList.add('devices-settings-tab');
this.registerRequiredCSS('emulation/devicesSettingsTab.css', {enableLegacyPatching: true});
const header = this.element.createChild('header');
UI.UIUtils.createTextChild(header.createChild('h1'), ls`Emulated Devices`);
this.containerElement = this.element.createChild('div', 'settings-container-wrapper')
.createChild('div', 'settings-tab settings-content settings-container');
const buttonsRow = this.containerElement.createChild('div', 'devices-button-row');
this._addCustomButton =
UI.UIUtils.createTextButton(Common.UIString.UIString('Add custom device...'), this._addCustomDevice.bind(this));
this._addCustomButton.id = 'custom-device-add-button';
buttonsRow.appendChild(this._addCustomButton);
this._list = new UI.ListWidget.ListWidget(this, false /* delegatesFocus */);
this._list.registerRequiredCSS('emulation/devicesSettingsTab.css', {enableLegacyPatching: true});
this._list.element.classList.add('devices-list');
this._list.show(this.containerElement);
this._muteUpdate = false;
this._emulatedDevicesList = EmulatedDevicesList.instance();
this._emulatedDevicesList.addEventListener(Events.CustomDevicesUpdated, this._devicesUpdated, this);
this._emulatedDevicesList.addEventListener(Events.StandardDevicesUpdated, this._devicesUpdated, this);
this.setDefaultFocusedElement(this._addCustomButton);
}
static instance() {
if (!devicesSettingsTabInstance) {
devicesSettingsTabInstance = new DevicesSettingsTab();
}
return devicesSettingsTabInstance;
}
/**
* @override
*/
wasShown() {
super.wasShown();
this._devicesUpdated();
}
_devicesUpdated() {
if (this._muteUpdate) {
return;
}
this._list.clear();
let devices = this._emulatedDevicesList.custom().slice();
for (let i = 0; i < devices.length; ++i) {
this._list.appendItem(devices[i], true);
}
this._list.appendSeparator();
devices = this._emulatedDevicesList.standard().slice();
devices.sort(EmulatedDevice.deviceComparator);
for (let i = 0; i < devices.length; ++i) {
this._list.appendItem(devices[i], false);
}
}
/**
* @param {boolean} custom
*/
_muteAndSaveDeviceList(custom) {
this._muteUpdate = true;
if (custom) {
this._emulatedDevicesList.saveCustomDevices();
} else {
this._emulatedDevicesList.saveStandardDevices();
}
this._muteUpdate = false;
}
_addCustomDevice() {
const device = new EmulatedDevice();
device.deviceScaleFactor = 0;
device.horizontal.width = 700;
device.horizontal.height = 400;
device.vertical.width = 400;
device.vertical.height = 700;
this._list.addNewItem(this._emulatedDevicesList.custom().length, device);
}
/**
* @param {number} value
* @return {string}
*/
_toNumericInputValue(value) {
return value ? String(value) : '';
}
/**
* @override
* @param {!EmulatedDevice} device
* @param {boolean} editable
* @return {!Element}
*/
renderItem(device, editable) {
const label = document.createElement('label');
label.classList.add('devices-list-item');
const checkbox = /** @type {!HTMLInputElement}*/ (label.createChild('input', 'devices-list-checkbox'));
checkbox.type = 'checkbox';
checkbox.checked = device.show();
checkbox.addEventListener('click', onItemClicked.bind(this), false);
const span = document.createElement('span');
span.classList.add('device-name');
span.appendChild(document.createTextNode(device.title));
label.appendChild(span);
return label;
/**
* @param {!Event} event
* @this {DevicesSettingsTab}
*/
function onItemClicked(event) {
const show = checkbox.checked;
device.setShow(show);
this._muteAndSaveDeviceList(editable);
event.consume();
}
}
/**
* @override
* @param {*} item
* @param {number} index
*/
removeItemRequested(item, index) {
this._emulatedDevicesList.removeCustomDevice(/** @type {!EmulatedDevice} */ (item));
}
/**
* @override
* @param {!EmulatedDevice} device
* @param {!UI.ListWidget.Editor<!EmulatedDevice>} editor
* @param {boolean} isNew
*/
commitEdit(device, editor, isNew) {
device.title = editor.control('title').value.trim();
device.vertical.width = editor.control('width').value ? parseInt(editor.control('width').value, 10) : 0;
device.vertical.height = editor.control('height').value ? parseInt(editor.control('height').value, 10) : 0;
device.horizontal.width = device.vertical.height;
device.horizontal.height = device.vertical.width;
device.deviceScaleFactor = editor.control('scale').value ? parseFloat(editor.control('scale').value) : 0;
device.userAgent = editor.control('user-agent').value;
device.modes = [];
device.modes.push({title: '', orientation: Vertical, insets: new UI.Geometry.Insets(0, 0, 0, 0), image: null});
device.modes.push({title: '', orientation: Horizontal, insets: new UI.Geometry.Insets(0, 0, 0, 0), image: null});
device.capabilities = [];
const uaType = editor.control('ua-type').value;
if (uaType === UA.Mobile || uaType === UA.MobileNoTouch) {
device.capabilities.push(Capability.Mobile);
}
if (uaType === UA.Mobile || uaType === UA.DesktopTouch) {
device.capabilities.push(Capability.Touch);
}
const brandsOrError = parseBrandsList(editor.control('brands').value.trim(), 'unused_err1', 'unused_err2');
device.userAgentMetadata = {
brands: (typeof brandsOrError === 'string' ? [] : brandsOrError),
fullVersion: editor.control('full-version').value.trim(),
platform: editor.control('platform').value.trim(),
platformVersion: editor.control('platform-version').value.trim(),
architecture: editor.control('arch').value.trim(),
model: editor.control('model').value.trim(),
mobile: (uaType === UA.Mobile || uaType === UA.MobileNoTouch)
};
if (isNew) {
this._emulatedDevicesList.addCustomDevice(device);
} else {
this._emulatedDevicesList.saveCustomDevices();
}
this._addCustomButton.scrollIntoViewIfNeeded();
this._addCustomButton.focus();
}
/**
* @override
* @param {!EmulatedDevice} device
* @return {!UI.ListWidget.Editor<!EmulatedDevice>}
*/
beginEdit(device) {
const editor = this._createEditor();
editor.control('title').value = device.title;
editor.control('width').value = this._toNumericInputValue(device.vertical.width);
editor.control('height').value = this._toNumericInputValue(device.vertical.height);
editor.control('scale').value = this._toNumericInputValue(device.deviceScaleFactor);
editor.control('user-agent').value = device.userAgent;
let uaType;
if (device.mobile()) {
uaType = device.touch() ? UA.Mobile : UA.MobileNoTouch;
} else {
uaType = device.touch() ? UA.DesktopTouch : UA.Desktop;
}
editor.control('ua-type').value = uaType;
if (device.userAgentMetadata) {
editor.control('brands').value = serializeBrandsList(device.userAgentMetadata.brands || []);
editor.control('full-version').value = device.userAgentMetadata.fullVersion || '';
editor.control('platform').value = device.userAgentMetadata.platform;
editor.control('platform-version').value = device.userAgentMetadata.platformVersion;
editor.control('arch').value = device.userAgentMetadata.architecture;
editor.control('model').value = device.userAgentMetadata.model;
}
return editor;
}
/**
* @return {!UI.ListWidget.Editor<!EmulatedDevice>}
*/
_createEditor() {
if (this._editor) {
return this._editor;
}
const editor = new UI.ListWidget.Editor();
this._editor = editor;
const content = editor.contentElement();
const deviceFields = content.createChild('div', 'devices-edit-fields');
UI.UIUtils.createTextChild(deviceFields.createChild('b'), ls`Device`);
const deviceNameField = editor.createInput('title', 'text', ls`Device Name`, titleValidator);
deviceFields.createChild('div', 'hbox').appendChild(deviceNameField);
deviceNameField.id = 'custom-device-name-field';
const screen = deviceFields.createChild('div', 'hbox');
screen.appendChild(editor.createInput('width', 'text', ls`Width`, widthValidator));
screen.appendChild(editor.createInput('height', 'text', ls`Height`, heightValidator));
const dpr = editor.createInput('scale', 'text', ls`Device pixel ratio`, scaleValidator);
dpr.classList.add('device-edit-fixed');
screen.appendChild(dpr);
const uaStringFields = content.createChild('div', 'devices-edit-fields');
UI.UIUtils.createTextChild(uaStringFields.createChild('b'), ls`User agent string`);
const ua = uaStringFields.createChild('div', 'hbox');
ua.appendChild(editor.createInput('user-agent', 'text', ls`User agent string`, () => {
return {valid: true, errorMessage: undefined};
}));
const uaTypeOptions = [UA.Mobile, UA.MobileNoTouch, UA.Desktop, UA.DesktopTouch];
const uaType = editor.createSelect('ua-type', uaTypeOptions, () => {
return {valid: true, errorMessage: undefined};
}, ls`User agent type`);
uaType.classList.add('device-edit-fixed');
ua.appendChild(uaType);
const uaChFields = content.createChild('div', 'devices-edit-client-hints-heading');
UI.UIUtils.createTextChild(uaChFields.createChild('b'), ls`User agent client hints`);
const helpIconWrapper = document.createElement('a');
helpIconWrapper.href = 'https://web.dev/user-agent-client-hints/';
helpIconWrapper.target = '_blank';
const icon = UI.Icon.Icon.create('mediumicon-info', 'help-icon');
helpIconWrapper.appendChild(icon);
helpIconWrapper.title =
ls`User agent client hints are an alternative to the user agent string that identify the browser and the device in a more structured way with better privacy accounting. Click the button to learn more.`;
// Prevent the editor grabbing the enter key, letting the default behavior happen.
helpIconWrapper.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.stopPropagation();
}
});
uaChFields.appendChild(helpIconWrapper);
const tree = new UI.TreeOutline.TreeOutlineInShadow();
tree.registerRequiredCSS('emulation/devicesSettingsTab.css', {enableLegacyPatching: true});
tree.setShowSelectionOnKeyboardFocus(true, false);
const treeRoot = new UI.TreeOutline.TreeElement(uaChFields, true);
tree.appendChild(treeRoot);
// Select the folder to make left/right arrows work as expected; don't change focus, however, since it should start with the device name field.
treeRoot.select(true, false);
content.appendChild(tree.element);
/**
* @param {!HTMLInputElement|!HTMLSelectElement} input
*/
function addToTree(input) {
const treeNode = new UI.TreeOutline.TreeElement(input, false);
// The inputs themselves are selectable, no need for the tree nodes to be.
treeNode.selectable = false;
treeNode.listItemElement.classList.add('devices-edit-client-hints-field');
treeRoot.appendChild(treeNode);
}
const brands =
editor.createInput('brands', 'text', ls`UA brands list (e.g. "Chromium";v="87")`, brandListValidator);
addToTree(brands);
const fullVersion =
editor.createInput('full-version', 'text', ls`Full browser version (e.g. 87.0.4280.88)`, chStringValidator);
addToTree(fullVersion);
const platform = editor.createInput('platform', 'text', ls`Platform (e.g. Android)`, chStringValidator);
addToTree(platform);
const platformVersion = editor.createInput('platform-version', 'text', ls`Platform version`, chStringValidator);
addToTree(platformVersion);
const arch = editor.createInput('arch', 'text', ls`Architecture (e.g. x86)`, chStringValidator);
addToTree(arch);
const model = editor.createInput('model', 'text', ls`Device model`, chStringValidator);
addToTree(model);
return editor;
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {!UI.ListWidget.ValidatorResult}
*/
function chStringValidator(item, index, input) {
return validateAsStructuredHeadersString(input.value, ls`Not representable as structured headers string.`);
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {!UI.ListWidget.ValidatorResult}
*/
function brandListValidator(item, index, input) {
const syntaxError = ls`Brands list is not a valid structured fields list.`;
const structError = ls`Brands list must consist of strings, each with a v parameter with a string value.`;
const errorOrResult = parseBrandsList(input.value, syntaxError, structError);
if (typeof errorOrResult === 'string') {
return {valid: false, errorMessage: errorOrResult};
}
return {valid: true, errorMessage: undefined};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {!UI.ListWidget.ValidatorResult}
*/
function titleValidator(item, index, input) {
let valid = false;
let errorMessage;
const value = input.value.trim();
if (value.length >= MaxDeviceNameLength) {
errorMessage = ls`Device name must be less than ${MaxDeviceNameLength} characters.`;
} else if (value.length === 0) {
errorMessage = ls`Device name cannot be empty.`;
} else {
valid = true;
}
return {valid, errorMessage};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {!UI.ListWidget.ValidatorResult}
*/
function widthValidator(item, index, input) {
return DeviceModeModel.widthValidator(input.value);
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {!UI.ListWidget.ValidatorResult}
*/
function heightValidator(item, index, input) {
return DeviceModeModel.heightValidator(input.value);
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @return {!UI.ListWidget.ValidatorResult}
*/
function scaleValidator(item, index, input) {
return DeviceModeModel.scaleValidator(input.value);
}
}
}