UNPKG

chrome-devtools-frontend

Version:
384 lines (338 loc) • 16.6 kB
// Copyright 2018 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 '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as UI from '../../ui/legacy/legacy.js'; import locationsSettingsTabStyles from './locationsSettingsTab.css.js'; let locationsSettingsTabInstance: LocationsSettingsTab; const UIStrings = { /** *@description Title in the Locations Settings Tab, where custom geographic locations that the user *has entered are stored. */ customLocations: 'Custom locations', /** *@description Label for the name of a geographic location that the user has entered. */ locationName: 'Location name', /** *@description Abbreviation of latitude in Locations Settings Tab of the Device Toolbar */ lat: 'Lat', /** *@description Abbreviation of longitude in Locations Settings Tab of the Device Toolbar */ long: 'Long', /** *@description Text in Sensors View of the Device Toolbar */ timezoneId: 'Timezone ID', /** *@description Label for text input for the locale of a particular location. */ locale: 'Locale', /** *@description Label for text input for the latitude of a GPS position. */ latitude: 'Latitude', /** *@description Label for text input for the longitude of a GPS position. */ longitude: 'Longitude', /** *@description Error message in the Locations settings pane that declares the location name input must not be empty */ locationNameCannotBeEmpty: 'Location name cannot be empty', /** *@description Error message in the Locations settings pane that declares the maximum length of the location name *@example {50} PH1 */ locationNameMustBeLessThanS: 'Location name must be less than {PH1} characters', /** *@description Error message in the Locations settings pane that declares that the value for the latitude input must be a number */ latitudeMustBeANumber: 'Latitude must be a number', /** *@description Error message in the Locations settings pane that declares the minimum value for the latitude input *@example {-90} PH1 */ latitudeMustBeGreaterThanOrEqual: 'Latitude must be greater than or equal to {PH1}', /** *@description Error message in the Locations settings pane that declares the maximum value for the latitude input *@example {90} PH1 */ latitudeMustBeLessThanOrEqualToS: 'Latitude must be less than or equal to {PH1}', /** *@description Error message in the Locations settings pane that declares that the value for the longitude input must be a number */ longitudeMustBeANumber: 'Longitude must be a number', /** *@description Error message in the Locations settings pane that declares the minimum value for the longitude input *@example {-180} PH1 */ longitudeMustBeGreaterThanOr: 'Longitude must be greater than or equal to {PH1}', /** *@description Error message in the Locations settings pane that declares the maximum value for the longitude input *@example {180} PH1 */ longitudeMustBeLessThanOrEqualTo: 'Longitude must be less than or equal to {PH1}', /** *@description Error message in the Locations settings pane that declares timezone ID input invalid */ timezoneIdMustContainAlphabetic: 'Timezone ID must contain alphabetic characters', /** *@description Error message in the Locations settings pane that declares locale input invalid */ localeMustContainAlphabetic: 'Locale must contain alphabetic characters', /** *@description Text of add locations button in Locations Settings Tab of the Device Toolbar */ addLocation: 'Add location...', }; const str_ = i18n.i18n.registerUIStrings('panels/sensors/LocationsSettingsTab.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class LocationsSettingsTab extends UI.Widget.VBox implements UI.ListWidget.Delegate<LocationDescription> { private readonly list: UI.ListWidget.ListWidget<LocationDescription>; private readonly customSetting: Common.Settings.Setting<LocationDescription[]>; private editor?: UI.ListWidget.Editor<LocationDescription>; private constructor() { super(true); this.contentElement.createChild('div', 'header').textContent = i18nString(UIStrings.customLocations); const addButton = UI.UIUtils.createTextButton( i18nString(UIStrings.addLocation), this.addButtonClicked.bind(this), 'add-locations-button'); this.contentElement.appendChild(addButton); this.list = new UI.ListWidget.ListWidget(this); this.list.element.classList.add('locations-list'); this.list.show(this.contentElement); this.customSetting = Common.Settings.Settings.instance().moduleSetting<LocationDescription[]>('emulation.locations'); const list = this.customSetting.get().map(location => replaceLocationTitles(location, this.customSetting.defaultValue)); function replaceLocationTitles( location: LocationDescription, defaultValues: LocationDescription[]): LocationDescription { // This check is done for locations that might had been cached wrongly due to crbug.com/1171670. // Each of the default values would have been stored without a title if the user had added a new location // while the bug was present in the application. This means that getting the setting's default value with the `get` // method would return the default locations without a title. To cope with this, the setting values are // preemptively checked and corrected so that any default value mistakenly stored without a title is replaced // with the corresponding declared value in the pre-registered setting. if (!location.title) { const replacement = defaultValues.find( defaultLocation => defaultLocation.lat === location.lat && defaultLocation.long === location.long && defaultLocation.timezoneId === location.timezoneId && defaultLocation.locale === location.locale); if (!replacement) { console.error('Could not determine a location setting title'); } else { return replacement; } } return location; } this.customSetting.set(list); this.customSetting.addChangeListener(this.locationsUpdated, this); this.setDefaultFocusedElement(addButton); } static instance(): LocationsSettingsTab { if (!locationsSettingsTabInstance) { locationsSettingsTabInstance = new LocationsSettingsTab(); } return locationsSettingsTabInstance; } override wasShown(): void { super.wasShown(); this.registerCSSFiles([locationsSettingsTabStyles]); this.list.registerCSSFiles([locationsSettingsTabStyles]); this.locationsUpdated(); } private locationsUpdated(): void { this.list.clear(); const conditions = this.customSetting.get(); for (const condition of conditions) { this.list.appendItem(condition, true); } this.list.appendSeparator(); } private addButtonClicked(): void { this.list.addNewItem(this.customSetting.get().length, {title: '', lat: 0, long: 0, timezoneId: '', locale: ''}); } renderItem(location: LocationDescription, _editable: boolean): Element { const element = document.createElement('div'); element.classList.add('locations-list-item'); const title = element.createChild('div', 'locations-list-text locations-list-title'); const titleText = title.createChild('div', 'locations-list-title-text'); titleText.textContent = location.title; UI.Tooltip.Tooltip.install(titleText, location.title); element.createChild('div', 'locations-list-separator'); element.createChild('div', 'locations-list-text').textContent = String(location.lat); element.createChild('div', 'locations-list-separator'); element.createChild('div', 'locations-list-text').textContent = String(location.long); element.createChild('div', 'locations-list-separator'); element.createChild('div', 'locations-list-text').textContent = location.timezoneId; element.createChild('div', 'locations-list-separator'); element.createChild('div', 'locations-list-text').textContent = location.locale; return element; } removeItemRequested(item: LocationDescription, index: number): void { const list = this.customSetting.get(); list.splice(index, 1); this.customSetting.set(list); } commitEdit(location: LocationDescription, editor: UI.ListWidget.Editor<LocationDescription>, isNew: boolean): void { location.title = editor.control('title').value.trim(); const lat = editor.control('lat').value.trim(); location.lat = lat ? parseFloat(lat) : 0; const long = editor.control('long').value.trim(); location.long = long ? parseFloat(long) : 0; const timezoneId = editor.control('timezoneId').value.trim(); location.timezoneId = timezoneId; const locale = editor.control('locale').value.trim(); location.locale = locale; const list = this.customSetting.get(); if (isNew) { list.push(location); } this.customSetting.set(list); } beginEdit(location: LocationDescription): UI.ListWidget.Editor<LocationDescription> { const editor = this.createEditor(); editor.control('title').value = location.title; editor.control('lat').value = String(location.lat); editor.control('long').value = String(location.long); editor.control('timezoneId').value = location.timezoneId; editor.control('locale').value = location.locale; return editor; } private createEditor(): UI.ListWidget.Editor<LocationDescription> { if (this.editor) { return this.editor; } const editor = new UI.ListWidget.Editor<LocationDescription>(); this.editor = editor; const content = editor.contentElement(); const titles = content.createChild('div', 'locations-edit-row'); titles.createChild('div', 'locations-list-text locations-list-title').textContent = i18nString(UIStrings.locationName); titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.lat); titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.long); titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.timezoneId); titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.locale); const fields = content.createChild('div', 'locations-edit-row'); fields.createChild('div', 'locations-list-text locations-list-title locations-input-container') .appendChild(editor.createInput('title', 'text', i18nString(UIStrings.locationName), titleValidator)); fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); let cell = fields.createChild('div', 'locations-list-text locations-input-container'); cell.appendChild(editor.createInput('lat', 'text', i18nString(UIStrings.latitude), latValidator)); fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); cell = fields.createChild('div', 'locations-list-text locations-list-text-longitude locations-input-container'); cell.appendChild(editor.createInput('long', 'text', i18nString(UIStrings.longitude), longValidator)); fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); cell = fields.createChild('div', 'locations-list-text locations-input-container'); cell.appendChild(editor.createInput('timezoneId', 'text', i18nString(UIStrings.timezoneId), timezoneIdValidator)); fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); cell = fields.createChild('div', 'locations-list-text locations-input-container'); cell.appendChild(editor.createInput('locale', 'text', i18nString(UIStrings.locale), localeValidator)); return editor; function titleValidator( item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const maxLength = 50; const value = input.value.trim(); let errorMessage; if (!value.length) { errorMessage = i18nString(UIStrings.locationNameCannotBeEmpty); } else if (value.length > maxLength) { errorMessage = i18nString(UIStrings.locationNameMustBeLessThanS, {PH1: maxLength}); } if (errorMessage) { return {valid: false, errorMessage}; } return {valid: true, errorMessage: undefined}; } function latValidator( item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const minLat = -90; const maxLat = 90; const value = input.value.trim(); const parsedValue = Number(value); if (!value) { return {valid: true, errorMessage: undefined}; } let errorMessage; if (Number.isNaN(parsedValue)) { errorMessage = i18nString(UIStrings.latitudeMustBeANumber); } else if (parseFloat(value) < minLat) { errorMessage = i18nString(UIStrings.latitudeMustBeGreaterThanOrEqual, {PH1: minLat}); } else if (parseFloat(value) > maxLat) { errorMessage = i18nString(UIStrings.latitudeMustBeLessThanOrEqualToS, {PH1: maxLat}); } if (errorMessage) { return {valid: false, errorMessage}; } return {valid: true, errorMessage: undefined}; } function longValidator( item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const minLong = -180; const maxLong = 180; const value = input.value.trim(); const parsedValue = Number(value); if (!value) { return {valid: true, errorMessage: undefined}; } let errorMessage; if (Number.isNaN(parsedValue)) { errorMessage = i18nString(UIStrings.longitudeMustBeANumber); } else if (parseFloat(value) < minLong) { errorMessage = i18nString(UIStrings.longitudeMustBeGreaterThanOr, {PH1: minLong}); } else if (parseFloat(value) > maxLong) { errorMessage = i18nString(UIStrings.longitudeMustBeLessThanOrEqualTo, {PH1: maxLong}); } if (errorMessage) { return {valid: false, errorMessage}; } return {valid: true, errorMessage: undefined}; } function timezoneIdValidator( item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const value = input.value.trim(); // Chromium uses ICU's timezone implementation, which is very // liberal in what it accepts. ICU does not simply use an allowlist // but instead tries to make sense of the input, even for // weird-looking timezone IDs. There's not much point in validating // the input other than checking if it contains at least one // alphabetic character. The empty string resets the override, // and is accepted as well. if (value === '' || /[a-zA-Z]/.test(value)) { return {valid: true, errorMessage: undefined}; } const errorMessage = i18nString(UIStrings.timezoneIdMustContainAlphabetic); return {valid: false, errorMessage}; } function localeValidator( item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const value = input.value.trim(); // Similarly to timezone IDs, there's not much point in validating // input locales other than checking if it contains at least two // alphabetic characters. // https://unicode.org/reports/tr35/#Unicode_language_identifier // The empty string resets the override, and is accepted as // well. if (value === '' || /[a-zA-Z]{2}/.test(value)) { return {valid: true, errorMessage: undefined}; } const errorMessage = i18nString(UIStrings.localeMustContainAlphabetic); return {valid: false, errorMessage}; } } } export interface LocationDescription { title: string; lat: number; long: number; timezoneId: string; locale: string; }