| // 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. |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import * as EmulationComponents from './components/components.js'; |
| |
| import {DeviceModeModel, MaxDeviceNameLength, UA} from './DeviceModeModel.js'; |
| import {Capability, EmulatedDevice, EmulatedDevicesList, Events, Horizontal, Vertical} from './EmulatedDevices.js'; |
| |
| let devicesSettingsTabInstance: DevicesSettingsTab; |
| |
| const UIStrings = { |
| /** |
| *@description Title for a section of the UI that shows all of the devices the user can emulate, in the Device Toolbar. |
| */ |
| emulatedDevices: 'Emulated Devices', |
| /** |
| *@description Button to add a custom device (e.g. phone, tablet) the Device Toolbar. |
| */ |
| addCustomDevice: 'Add custom device...', |
| /** |
| *@description Label/title for UI to add a new custom device type. Device means mobile/tablet etc. |
| */ |
| device: 'Device', |
| /** |
| *@description Placeholder for text input for the name of a custom device. |
| */ |
| deviceName: 'Device Name', |
| /** |
| *@description Placeholder text for text input for the width of a custom device in pixels. |
| */ |
| width: 'Width', |
| /** |
| *@description Placeholder text for text input for the height of a custom device in pixels. |
| */ |
| height: 'Height', |
| /** |
| *@description Placeholder text for text input for the height/width ratio of a custom device in pixels. |
| */ |
| devicePixelRatio: 'Device pixel ratio', |
| /** |
| *@description Label in the Devices settings pane for the user agent string input of a custom device |
| */ |
| userAgentString: 'User agent string', |
| /** |
| *@description Tooltip text for a drop-down in the Devices settings pane, for the 'user agent type' input of a custom device. |
| * 'Type' refers to different options e.g. mobile or desktop. |
| */ |
| userAgentType: 'User agent type', |
| /** |
| *@description Error message in the Devices settings pane that declares the maximum length of the device name input |
| *@example {50} PH1 |
| */ |
| deviceNameMustBeLessThanS: 'Device name must be less than {PH1} characters.', |
| /** |
| *@description Error message in the Devices settings pane that declares that the device name input must not be empty |
| */ |
| deviceNameCannotBeEmpty: 'Device name cannot be empty.', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('panels/emulation/DevicesSettingsTab.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export class DevicesSettingsTab extends UI.Widget.VBox implements UI.ListWidget.Delegate<EmulatedDevice> { |
| containerElement: HTMLElement; |
| _addCustomButton: HTMLButtonElement; |
| _list: UI.ListWidget.ListWidget<EmulatedDevice>; |
| _muteUpdate: boolean; |
| _emulatedDevicesList: EmulatedDevicesList; |
| _editor?: UI.ListWidget.Editor<EmulatedDevice>; |
| |
| private constructor() { |
| super(); |
| this.element.classList.add('settings-tab-container'); |
| this.element.classList.add('devices-settings-tab'); |
| this.registerRequiredCSS('panels/emulation/devicesSettingsTab.css', {enableLegacyPatching: false}); |
| |
| const header = this.element.createChild('header'); |
| UI.UIUtils.createTextChild(header.createChild('h1'), i18nString(UIStrings.emulatedDevices)); |
| 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(i18nString(UIStrings.addCustomDevice), 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('panels/emulation/devicesSettingsTab.css', {enableLegacyPatching: false}); |
| 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(): DevicesSettingsTab { |
| if (!devicesSettingsTabInstance) { |
| devicesSettingsTabInstance = new DevicesSettingsTab(); |
| } |
| return devicesSettingsTabInstance; |
| } |
| |
| wasShown(): void { |
| super.wasShown(); |
| this._devicesUpdated(); |
| } |
| |
| _devicesUpdated(): void { |
| 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); |
| } |
| } |
| |
| _muteAndSaveDeviceList(custom: boolean): void { |
| this._muteUpdate = true; |
| if (custom) { |
| this._emulatedDevicesList.saveCustomDevices(); |
| } else { |
| this._emulatedDevicesList.saveStandardDevices(); |
| } |
| this._muteUpdate = false; |
| } |
| |
| _addCustomDevice(): void { |
| 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); |
| } |
| |
| _toNumericInputValue(value: number): string { |
| return value ? String(value) : ''; |
| } |
| |
| renderItem(device: EmulatedDevice, editable: boolean): Element { |
| const label = document.createElement('label'); |
| label.classList.add('devices-list-item'); |
| const checkbox = (label.createChild('input', 'devices-list-checkbox') as HTMLInputElement); |
| 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; |
| |
| function onItemClicked(this: DevicesSettingsTab, event: Event): void { |
| const show = checkbox.checked; |
| device.setShow(show); |
| this._muteAndSaveDeviceList(editable); |
| event.consume(); |
| } |
| } |
| |
| removeItemRequested(item: EmulatedDevice): void { |
| this._emulatedDevicesList.removeCustomDevice(item); |
| } |
| |
| commitEdit(device: EmulatedDevice, editor: UI.ListWidget.Editor<EmulatedDevice>, isNew: boolean): void { |
| 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 userAgentControlValue = |
| (editor.control('ua-metadata') as |
| UI.ListWidget.CustomEditorControl<EmulationComponents.UserAgentClientHintsForm.UserAgentClientHintsFormData>) |
| .value.metaData; |
| if (userAgentControlValue) { |
| device.userAgentMetadata = { |
| ...userAgentControlValue, |
| mobile: (uaType === UA.Mobile || uaType === UA.MobileNoTouch), |
| }; |
| } |
| if (isNew) { |
| this._emulatedDevicesList.addCustomDevice(device); |
| } else { |
| this._emulatedDevicesList.saveCustomDevices(); |
| } |
| this._addCustomButton.scrollIntoViewIfNeeded(); |
| this._addCustomButton.focus(); |
| } |
| |
| beginEdit(device: EmulatedDevice): UI.ListWidget.Editor<EmulatedDevice> { |
| 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; |
| (editor.control('ua-metadata') as |
| UI.ListWidget.CustomEditorControl<EmulationComponents.UserAgentClientHintsForm.UserAgentClientHintsFormData>) |
| .value = {metaData: device.userAgentMetadata || undefined}; |
| return editor; |
| } |
| |
| _createEditor(): UI.ListWidget.Editor<EmulatedDevice> { |
| if (this._editor) { |
| return this._editor; |
| } |
| |
| const editor = new UI.ListWidget.Editor<EmulatedDevice>(); |
| this._editor = editor; |
| const content = editor.contentElement(); |
| |
| const deviceFields = content.createChild('div', 'devices-edit-fields'); |
| UI.UIUtils.createTextChild(deviceFields.createChild('b'), i18nString(UIStrings.device)); |
| const deviceNameField = editor.createInput('title', 'text', i18nString(UIStrings.deviceName), 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', i18nString(UIStrings.width), widthValidator)); |
| screen.appendChild(editor.createInput('height', 'text', i18nString(UIStrings.height), heightValidator)); |
| const dpr = editor.createInput('scale', 'text', i18nString(UIStrings.devicePixelRatio), scaleValidator); |
| dpr.classList.add('device-edit-fixed'); |
| screen.appendChild(dpr); |
| |
| const uaStringFields = content.createChild('div', 'devices-edit-fields'); |
| UI.UIUtils.createTextChild(uaStringFields.createChild('b'), i18nString(UIStrings.userAgentString)); |
| |
| const ua = uaStringFields.createChild('div', 'hbox'); |
| ua.appendChild(editor.createInput('user-agent', 'text', i18nString(UIStrings.userAgentString), () => { |
| 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}; |
| }, i18nString(UIStrings.userAgentType)); |
| uaType.classList.add('device-edit-fixed'); |
| ua.appendChild(uaType); |
| |
| const uaMetadata = editor.createCustomControl( |
| 'ua-metadata', EmulationComponents.UserAgentClientHintsForm.UserAgentClientHintsForm, |
| userAgentMetadataValidator); |
| uaMetadata.value = {}; |
| uaMetadata.addEventListener('clienthintschange', () => editor.requestValidation(), false); |
| content.appendChild(uaMetadata); |
| |
| return editor; |
| |
| function userAgentMetadataValidator(): UI.ListWidget.ValidatorResult { |
| return uaMetadata.validate(); |
| } |
| |
| function titleValidator( |
| item: EmulatedDevice, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
| let valid = false; |
| let errorMessage; |
| |
| const value = input.value.trim(); |
| if (value.length >= MaxDeviceNameLength) { |
| errorMessage = i18nString(UIStrings.deviceNameMustBeLessThanS, {PH1: MaxDeviceNameLength}); |
| } else if (value.length === 0) { |
| errorMessage = i18nString(UIStrings.deviceNameCannotBeEmpty); |
| } else { |
| valid = true; |
| } |
| |
| return {valid, errorMessage}; |
| } |
| |
| function widthValidator( |
| item: EmulatedDevice, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
| return DeviceModeModel.widthValidator(input.value); |
| } |
| |
| function heightValidator( |
| item: EmulatedDevice, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
| return DeviceModeModel.heightValidator(input.value); |
| } |
| |
| function scaleValidator( |
| item: EmulatedDevice, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
| return DeviceModeModel.scaleValidator(input.value); |
| } |
| } |
| } |