Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 1 | // Copyright 2018 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Tim van der Lippe | 7696157 | 2021-04-06 10:48:07 | [diff] [blame] | 5 | import * as Common from '../../core/common/common.js'; |
Tim van der Lippe | bb352e6 | 2021-04-01 17:57:28 | [diff] [blame] | 6 | import * as i18n from '../../core/i18n/i18n.js'; |
Tim van der Lippe | aa61faf | 2021-04-07 15:32:07 | [diff] [blame] | 7 | import * as UI from '../../ui/legacy/legacy.js'; |
Tim van der Lippe | 4866dfa | 2021-08-02 16:09:10 | [diff] [blame] | 8 | import locationsSettingsTabStyles from './locationsSettingsTab.css.js'; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 9 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 10 | let locationsSettingsTabInstance: LocationsSettingsTab; |
Tim van der Lippe | 4b527cf | 2020-12-14 15:37:51 | [diff] [blame] | 11 | |
Simon Zünd | 6f95e84 | 2021-03-01 07:41:55 | [diff] [blame] | 12 | const UIStrings = { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 13 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 14 | *@description Title in the Locations Settings Tab, where custom geographic locations that the user |
| 15 | *has entered are stored. |
| 16 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 17 | customLocations: 'Custom locations', |
| 18 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 19 | *@description Label for the name of a geographic location that the user has entered. |
| 20 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 21 | locationName: 'Location name', |
| 22 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 23 | *@description Abbreviation of latitude in Locations Settings Tab of the Device Toolbar |
| 24 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 25 | lat: 'Lat', |
| 26 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 27 | *@description Abbreviation of longitude in Locations Settings Tab of the Device Toolbar |
| 28 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 29 | long: 'Long', |
| 30 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 31 | *@description Text in Sensors View of the Device Toolbar |
| 32 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 33 | timezoneId: 'Timezone ID', |
| 34 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 35 | *@description Label for text input for the locale of a particular location. |
| 36 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 37 | locale: 'Locale', |
| 38 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 39 | *@description Label for text input for the latitude of a GPS position. |
| 40 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 41 | latitude: 'Latitude', |
| 42 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 43 | *@description Label for text input for the longitude of a GPS position. |
| 44 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 45 | longitude: 'Longitude', |
| 46 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 47 | *@description Error message in the Locations settings pane that declares the location name input must not be empty |
| 48 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 49 | locationNameCannotBeEmpty: 'Location name cannot be empty', |
| 50 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 51 | *@description Error message in the Locations settings pane that declares the maximum length of the location name |
| 52 | *@example {50} PH1 |
| 53 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 54 | locationNameMustBeLessThanS: 'Location name must be less than {PH1} characters', |
| 55 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 56 | *@description Error message in the Locations settings pane that declares that the value for the latitude input must be a number |
| 57 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 58 | latitudeMustBeANumber: 'Latitude must be a number', |
| 59 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 60 | *@description Error message in the Locations settings pane that declares the minimum value for the latitude input |
| 61 | *@example {-90} PH1 |
| 62 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 63 | latitudeMustBeGreaterThanOrEqual: 'Latitude must be greater than or equal to {PH1}', |
| 64 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 65 | *@description Error message in the Locations settings pane that declares the maximum value for the latitude input |
| 66 | *@example {90} PH1 |
| 67 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 68 | latitudeMustBeLessThanOrEqualToS: 'Latitude must be less than or equal to {PH1}', |
| 69 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 70 | *@description Error message in the Locations settings pane that declares that the value for the longitude input must be a number |
| 71 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 72 | longitudeMustBeANumber: 'Longitude must be a number', |
| 73 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 74 | *@description Error message in the Locations settings pane that declares the minimum value for the longitude input |
| 75 | *@example {-180} PH1 |
| 76 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 77 | longitudeMustBeGreaterThanOr: 'Longitude must be greater than or equal to {PH1}', |
| 78 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 79 | *@description Error message in the Locations settings pane that declares the maximum value for the longitude input |
| 80 | *@example {180} PH1 |
| 81 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 82 | longitudeMustBeLessThanOrEqualTo: 'Longitude must be less than or equal to {PH1}', |
| 83 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 84 | *@description Error message in the Locations settings pane that declares timezone ID input invalid |
| 85 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 86 | timezoneIdMustContainAlphabetic: 'Timezone ID must contain alphabetic characters', |
| 87 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 88 | *@description Error message in the Locations settings pane that declares locale input invalid |
| 89 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 90 | localeMustContainAlphabetic: 'Locale must contain alphabetic characters', |
| 91 | /** |
Jack Franklin | fd72c07 | 2022-12-21 11:45:01 | [diff] [blame^] | 92 | *@description Text of add locations button in Locations Settings Tab of the Device Toolbar |
| 93 | */ |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 94 | addLocation: 'Add location...', |
| 95 | }; |
Tim van der Lippe | 4866dfa | 2021-08-02 16:09:10 | [diff] [blame] | 96 | const str_ = i18n.i18n.registerUIStrings('panels/sensors/LocationsSettingsTab.ts', UIStrings); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 97 | const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| 98 | |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 99 | export class LocationsSettingsTab extends UI.Widget.VBox implements UI.ListWidget.Delegate<LocationDescription> { |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 100 | private readonly list: UI.ListWidget.ListWidget<LocationDescription>; |
| 101 | private readonly customSetting: Common.Settings.Setting<LocationDescription[]>; |
| 102 | private editor?: UI.ListWidget.Editor<LocationDescription>; |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 103 | |
| 104 | private constructor() { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 105 | super(true); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 106 | |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 107 | this.contentElement.createChild('div', 'header').textContent = i18nString(UIStrings.customLocations); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 108 | |
| 109 | const addButton = UI.UIUtils.createTextButton( |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 110 | i18nString(UIStrings.addLocation), this.addButtonClicked.bind(this), 'add-locations-button'); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 111 | this.contentElement.appendChild(addButton); |
| 112 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 113 | this.list = new UI.ListWidget.ListWidget(this); |
| 114 | this.list.element.classList.add('locations-list'); |
| 115 | this.list.show(this.contentElement); |
| 116 | this.customSetting = |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 117 | Common.Settings.Settings.instance().moduleSetting<LocationDescription[]>('emulation.locations'); |
| 118 | const list = |
Simon Zünd | a7f6c98 | 2021-10-05 10:58:02 | [diff] [blame] | 119 | this.customSetting.get().map(location => replaceLocationTitles(location, this.customSetting.defaultValue)); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 120 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 121 | function replaceLocationTitles( |
| 122 | location: LocationDescription, defaultValues: LocationDescription[]): LocationDescription { |
Andres Olivares | ceb3ec2 | 2021-02-02 16:23:40 | [diff] [blame] | 123 | // This check is done for locations that might had been cached wrongly due to crbug.com/1171670. |
| 124 | // Each of the default values would have been stored without a title if the user had added a new location |
| 125 | // while the bug was present in the application. This means that getting the setting's default value with the `get` |
| 126 | // method would return the default locations without a title. To cope with this, the setting values are |
| 127 | // preemptively checked and corrected so that any default value mistakenly stored without a title is replaced |
| 128 | // with the corresponding declared value in the pre-registered setting. |
| 129 | if (!location.title) { |
| 130 | const replacement = defaultValues.find( |
| 131 | defaultLocation => defaultLocation.lat === location.lat && defaultLocation.long === location.long && |
| 132 | defaultLocation.timezoneId === location.timezoneId && defaultLocation.locale === location.locale); |
| 133 | if (!replacement) { |
| 134 | console.error('Could not determine a location setting title'); |
| 135 | } else { |
| 136 | return replacement; |
| 137 | } |
| 138 | } |
| 139 | return location; |
| 140 | } |
| 141 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 142 | this.customSetting.set(list); |
| 143 | this.customSetting.addChangeListener(this.locationsUpdated, this); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 144 | |
| 145 | this.setDefaultFocusedElement(addButton); |
| 146 | } |
| 147 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 148 | static instance(): LocationsSettingsTab { |
Tim van der Lippe | 4b527cf | 2020-12-14 15:37:51 | [diff] [blame] | 149 | if (!locationsSettingsTabInstance) { |
| 150 | locationsSettingsTabInstance = new LocationsSettingsTab(); |
| 151 | } |
| 152 | |
| 153 | return locationsSettingsTabInstance; |
| 154 | } |
| 155 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 156 | wasShown(): void { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 157 | super.wasShown(); |
Tim van der Lippe | 4866dfa | 2021-08-02 16:09:10 | [diff] [blame] | 158 | this.registerCSSFiles([locationsSettingsTabStyles]); |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 159 | this.list.registerCSSFiles([locationsSettingsTabStyles]); |
| 160 | this.locationsUpdated(); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 161 | } |
| 162 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 163 | private locationsUpdated(): void { |
| 164 | this.list.clear(); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 165 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 166 | const conditions = this.customSetting.get(); |
Andres Olivares | edcab58 | 2021-01-27 13:17:19 | [diff] [blame] | 167 | for (const condition of conditions) { |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 168 | this.list.appendItem(condition, true); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 169 | } |
| 170 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 171 | this.list.appendSeparator(); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 172 | } |
| 173 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 174 | private addButtonClicked(): void { |
| 175 | this.list.addNewItem(this.customSetting.get().length, {title: '', lat: 0, long: 0, timezoneId: '', locale: ''}); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 176 | } |
| 177 | |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 178 | renderItem(location: LocationDescription, _editable: boolean): Element { |
Tim van der Lippe | f49e232 | 2020-05-01 15:03:09 | [diff] [blame] | 179 | const element = document.createElement('div'); |
| 180 | element.classList.add('locations-list-item'); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 181 | const title = element.createChild('div', 'locations-list-text locations-list-title'); |
| 182 | const titleText = title.createChild('div', 'locations-list-title-text'); |
| 183 | titleText.textContent = location.title; |
Tim van der Lippe | 70842f3 | 2020-11-23 16:56:57 | [diff] [blame] | 184 | UI.Tooltip.Tooltip.install(titleText, location.title); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 185 | element.createChild('div', 'locations-list-separator'); |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 186 | element.createChild('div', 'locations-list-text').textContent = String(location.lat); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 187 | element.createChild('div', 'locations-list-separator'); |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 188 | element.createChild('div', 'locations-list-text').textContent = String(location.long); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 189 | element.createChild('div', 'locations-list-separator'); |
Mathias Bynens | 7767e4f | 2020-04-06 12:54:54 | [diff] [blame] | 190 | element.createChild('div', 'locations-list-text').textContent = location.timezoneId; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 191 | element.createChild('div', 'locations-list-separator'); |
| 192 | element.createChild('div', 'locations-list-text').textContent = location.locale; |
| 193 | return element; |
| 194 | } |
| 195 | |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 196 | removeItemRequested(item: LocationDescription, index: number): void { |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 197 | const list = this.customSetting.get(); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 198 | list.splice(index, 1); |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 199 | this.customSetting.set(list); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 200 | } |
| 201 | |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 202 | commitEdit(location: LocationDescription, editor: UI.ListWidget.Editor<LocationDescription>, isNew: boolean): void { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 203 | location.title = editor.control('title').value.trim(); |
| 204 | const lat = editor.control('lat').value.trim(); |
| 205 | location.lat = lat ? parseFloat(lat) : 0; |
| 206 | const long = editor.control('long').value.trim(); |
| 207 | location.long = long ? parseFloat(long) : 0; |
| 208 | const timezoneId = editor.control('timezoneId').value.trim(); |
| 209 | location.timezoneId = timezoneId; |
| 210 | const locale = editor.control('locale').value.trim(); |
| 211 | location.locale = locale; |
| 212 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 213 | const list = this.customSetting.get(); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 214 | if (isNew) { |
| 215 | list.push(location); |
| 216 | } |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 217 | this.customSetting.set(list); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 218 | } |
| 219 | |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 220 | beginEdit(location: LocationDescription): UI.ListWidget.Editor<LocationDescription> { |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 221 | const editor = this.createEditor(); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 222 | editor.control('title').value = location.title; |
| 223 | editor.control('lat').value = String(location.lat); |
| 224 | editor.control('long').value = String(location.long); |
| 225 | editor.control('timezoneId').value = location.timezoneId; |
| 226 | editor.control('locale').value = location.locale; |
| 227 | return editor; |
| 228 | } |
| 229 | |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 230 | private createEditor(): UI.ListWidget.Editor<LocationDescription> { |
| 231 | if (this.editor) { |
| 232 | return this.editor; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 233 | } |
| 234 | |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 235 | const editor = new UI.ListWidget.Editor<LocationDescription>(); |
Jan Scheffler | 2a0ff4e | 2021-08-10 10:15:04 | [diff] [blame] | 236 | this.editor = editor; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 237 | const content = editor.contentElement(); |
| 238 | |
| 239 | const titles = content.createChild('div', 'locations-edit-row'); |
| 240 | titles.createChild('div', 'locations-list-text locations-list-title').textContent = |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 241 | i18nString(UIStrings.locationName); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 242 | titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 243 | titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.lat); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 244 | titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 245 | titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.long); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 246 | titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 247 | titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.timezoneId); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 248 | titles.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 249 | titles.createChild('div', 'locations-list-text').textContent = i18nString(UIStrings.locale); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 250 | |
| 251 | const fields = content.createChild('div', 'locations-edit-row'); |
Alex Rudenko | ac1817c | 2020-04-08 09:57:56 | [diff] [blame] | 252 | fields.createChild('div', 'locations-list-text locations-list-title locations-input-container') |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 253 | .appendChild(editor.createInput('title', 'text', i18nString(UIStrings.locationName), titleValidator)); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 254 | fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
| 255 | |
Alex Rudenko | ac1817c | 2020-04-08 09:57:56 | [diff] [blame] | 256 | let cell = fields.createChild('div', 'locations-list-text locations-input-container'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 257 | cell.appendChild(editor.createInput('lat', 'text', i18nString(UIStrings.latitude), latValidator)); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 258 | fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
| 259 | |
Alex Rudenko | ac1817c | 2020-04-08 09:57:56 | [diff] [blame] | 260 | cell = fields.createChild('div', 'locations-list-text locations-list-text-longitude locations-input-container'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 261 | cell.appendChild(editor.createInput('long', 'text', i18nString(UIStrings.longitude), longValidator)); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 262 | fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
| 263 | |
Alex Rudenko | ac1817c | 2020-04-08 09:57:56 | [diff] [blame] | 264 | cell = fields.createChild('div', 'locations-list-text locations-input-container'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 265 | cell.appendChild(editor.createInput('timezoneId', 'text', i18nString(UIStrings.timezoneId), timezoneIdValidator)); |
Mathias Bynens | 7767e4f | 2020-04-06 12:54:54 | [diff] [blame] | 266 | fields.createChild('div', 'locations-list-separator locations-list-separator-invisible'); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 267 | |
Alex Rudenko | ac1817c | 2020-04-08 09:57:56 | [diff] [blame] | 268 | cell = fields.createChild('div', 'locations-list-text locations-input-container'); |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 269 | cell.appendChild(editor.createInput('locale', 'text', i18nString(UIStrings.locale), localeValidator)); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 270 | |
| 271 | return editor; |
| 272 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 273 | function titleValidator( |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 274 | item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 275 | const maxLength = 50; |
| 276 | const value = input.value.trim(); |
| 277 | |
| 278 | let errorMessage; |
| 279 | if (!value.length) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 280 | errorMessage = i18nString(UIStrings.locationNameCannotBeEmpty); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 281 | } else if (value.length > maxLength) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 282 | errorMessage = i18nString(UIStrings.locationNameMustBeLessThanS, {PH1: maxLength}); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 283 | } |
| 284 | |
| 285 | if (errorMessage) { |
| 286 | return {valid: false, errorMessage}; |
| 287 | } |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 288 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 289 | } |
| 290 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 291 | function latValidator( |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 292 | item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 293 | const minLat = -90; |
| 294 | const maxLat = 90; |
| 295 | const value = input.value.trim(); |
| 296 | const parsedValue = Number(value); |
| 297 | |
| 298 | if (!value) { |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 299 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 300 | } |
| 301 | |
| 302 | let errorMessage; |
| 303 | if (Number.isNaN(parsedValue)) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 304 | errorMessage = i18nString(UIStrings.latitudeMustBeANumber); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 305 | } else if (parseFloat(value) < minLat) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 306 | errorMessage = i18nString(UIStrings.latitudeMustBeGreaterThanOrEqual, {PH1: minLat}); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 307 | } else if (parseFloat(value) > maxLat) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 308 | errorMessage = i18nString(UIStrings.latitudeMustBeLessThanOrEqualToS, {PH1: maxLat}); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 309 | } |
| 310 | |
| 311 | if (errorMessage) { |
| 312 | return {valid: false, errorMessage}; |
| 313 | } |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 314 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 315 | } |
| 316 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 317 | function longValidator( |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 318 | item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 319 | const minLong = -180; |
| 320 | const maxLong = 180; |
| 321 | const value = input.value.trim(); |
| 322 | const parsedValue = Number(value); |
| 323 | |
| 324 | if (!value) { |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 325 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 326 | } |
| 327 | |
| 328 | let errorMessage; |
| 329 | if (Number.isNaN(parsedValue)) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 330 | errorMessage = i18nString(UIStrings.longitudeMustBeANumber); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 331 | } else if (parseFloat(value) < minLong) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 332 | errorMessage = i18nString(UIStrings.longitudeMustBeGreaterThanOr, {PH1: minLong}); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 333 | } else if (parseFloat(value) > maxLong) { |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 334 | errorMessage = i18nString(UIStrings.longitudeMustBeLessThanOrEqualTo, {PH1: maxLong}); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 335 | } |
| 336 | |
| 337 | if (errorMessage) { |
| 338 | return {valid: false, errorMessage}; |
| 339 | } |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 340 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 341 | } |
| 342 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 343 | function timezoneIdValidator( |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 344 | item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 345 | const value = input.value.trim(); |
| 346 | // Chromium uses ICU's timezone implementation, which is very |
| 347 | // liberal in what it accepts. ICU does not simply use an allowlist |
| 348 | // but instead tries to make sense of the input, even for |
| 349 | // weird-looking timezone IDs. There's not much point in validating |
| 350 | // the input other than checking if it contains at least one |
| 351 | // alphabetic character. The empty string resets the override, |
| 352 | // and is accepted as well. |
| 353 | if (value === '' || /[a-zA-Z]/.test(value)) { |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 354 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 355 | } |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 356 | const errorMessage = i18nString(UIStrings.timezoneIdMustContainAlphabetic); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 357 | return {valid: false, errorMessage}; |
| 358 | } |
| 359 | |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 360 | function localeValidator( |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 361 | item: LocationDescription, index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 362 | const value = input.value.trim(); |
| 363 | // Similarly to timezone IDs, there's not much point in validating |
| 364 | // input locales other than checking if it contains at least two |
| 365 | // alphabetic characters. |
| 366 | // https://unicode.org/reports/tr35/#Unicode_language_identifier |
| 367 | // The empty string resets the override, and is accepted as |
| 368 | // well. |
| 369 | if (value === '' || /[a-zA-Z]{2}/.test(value)) { |
Alex Rudenko | e093097 | 2020-10-09 09:00:34 | [diff] [blame] | 370 | return {valid: true, errorMessage: undefined}; |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 371 | } |
Kalon Hinds | 1fb8543 | 2021-02-26 19:30:47 | [diff] [blame] | 372 | const errorMessage = i18nString(UIStrings.localeMustContainAlphabetic); |
Mathias Bynens | 4e1cc67 | 2020-03-25 06:53:18 | [diff] [blame] | 373 | return {valid: false, errorMessage}; |
| 374 | } |
| 375 | } |
| 376 | } |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 377 | export interface LocationDescription { |
Sigurd Schneider | b3ab8f6 | 2021-07-16 06:19:49 | [diff] [blame] | 378 | title: string; |
Jan Scheffler | 050013a | 2021-04-13 14:11:02 | [diff] [blame] | 379 | lat: number; |
| 380 | long: number; |
| 381 | timezoneId: string; |
| 382 | locale: string; |
| 383 | } |