chrome-devtools-frontend 1.0.1628368 → 1.0.1629211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/front_end/core/host/UserMetrics.ts +0 -1
  2. package/front_end/core/root/ExperimentNames.ts +0 -2
  3. package/front_end/core/sdk/DOMModel.ts +0 -5
  4. package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +0 -2
  5. package/front_end/entrypoints/greendev_floaty/greendev_floaty.ts +0 -2
  6. package/front_end/entrypoints/main/MainImpl.ts +0 -3
  7. package/front_end/generated/InspectorBackendCommands.ts +2 -2
  8. package/front_end/generated/protocol.ts +5 -0
  9. package/front_end/models/emulation/DeviceModeModel.ts +4 -0
  10. package/front_end/models/issues_manager/GenericIssue.ts +18 -1
  11. package/front_end/models/issues_manager/descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  12. package/front_end/panels/common/freDialog.css +1 -0
  13. package/front_end/panels/elements/StylesSidebarPane.ts +1 -1
  14. package/front_end/panels/elements/elements-meta.ts +0 -25
  15. package/front_end/panels/elements/elements.ts +0 -3
  16. package/front_end/panels/emulation/DeviceModeToolbar.ts +335 -248
  17. package/front_end/panels/profiler/ProfilesPanel.ts +7 -1
  18. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +29 -18
  19. package/front_end/panels/sources/sources-meta.ts +7 -0
  20. package/front_end/third_party/chromium/README.chromium +1 -1
  21. package/front_end/ui/legacy/TabbedPane.ts +142 -16
  22. package/front_end/ui/legacy/TextPrompt.docs.ts +51 -0
  23. package/front_end/ui/legacy/TextPrompt.ts +55 -6
  24. package/front_end/ui/legacy/Toolbar.ts +2 -1
  25. package/front_end/ui/legacy/View.ts +2 -1
  26. package/front_end/ui/legacy/ViewManager.ts +11 -5
  27. package/front_end/ui/legacy/textPrompt.css +4 -0
  28. package/mcp/mcp.ts +1 -0
  29. package/package.json +1 -1
  30. package/front_end/panels/elements/NodeStackTraceWidget.ts +0 -76
  31. package/front_end/panels/elements/nodeStackTraceWidget.css +0 -11
@@ -1,8 +1,6 @@
1
1
  // Copyright 2016 The Chromium Authors
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
- /* eslint-disable @devtools/no-imperative-dom-api */
5
- /* eslint-disable @devtools/no-lit-render-outside-of-view */
6
4
 
7
5
  import '../../ui/legacy/legacy.js';
8
6
 
@@ -13,10 +11,58 @@ import * as Platform from '../../core/platform/platform.js';
13
11
  import * as EmulationModel from '../../models/emulation/emulation.js';
14
12
  import * as Buttons from '../../ui/components/buttons/buttons.js';
15
13
  import * as UI from '../../ui/legacy/legacy.js';
16
- import {Directives, html, i18nTemplate, type LitTemplate, render} from '../../ui/lit/lit.js';
14
+ import {Directive, Directives, html, i18nTemplate, type LitTemplate, noChange, render} from '../../ui/lit/lit.js';
17
15
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
18
16
  import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';
19
17
 
18
+ class AutoWidthSelectDirective extends Directive.Directive {
19
+ static itemWidthCache = new Map<string, string>();
20
+
21
+ constructor(partInfo: Directive.PartInfo) {
22
+ super(partInfo);
23
+ if (partInfo.type !== Directive.PartType.ELEMENT) {
24
+ throw new Error('AutoWidthSelectDirective must be used as an element directive');
25
+ }
26
+ }
27
+
28
+ override update(part: Directive.ElementPart, [text]: [string]): unknown {
29
+ /* eslint-disable @devtools/no-imperative-dom-api */
30
+ const select = part.element as HTMLSelectElement;
31
+
32
+ let widthPx = AutoWidthSelectDirective.itemWidthCache.get(text);
33
+ if (!widthPx) {
34
+ const measuringElement = document.createElement('select');
35
+ measuringElement.className = select.className;
36
+ measuringElement.style.width = 'fit-content';
37
+ measuringElement.style.position = 'absolute';
38
+ measuringElement.style.visibility = 'hidden';
39
+ measuringElement.style.pointerEvents = 'none';
40
+ measuringElement.appendChild(document.createElement('option'));
41
+ measuringElement.options[0].textContent = text;
42
+
43
+ // Append to the select's parent so it inherits the exact same Shadow DOM styles
44
+ select.parentElement?.appendChild(measuringElement);
45
+ const width = measuringElement.offsetWidth;
46
+ measuringElement.remove();
47
+
48
+ widthPx = width ? `${width}px` : '';
49
+ if (width > 0) {
50
+ AutoWidthSelectDirective.itemWidthCache.set(text, widthPx);
51
+ }
52
+ }
53
+
54
+ select.style.width = widthPx;
55
+ /* eslint-enable @devtools/no-imperative-dom-api */
56
+ return this.render(text);
57
+ }
58
+
59
+ render(_text: string): unknown {
60
+ return noChange;
61
+ }
62
+ }
63
+
64
+ const autoWidthSelect = Directive.directive(AutoWidthSelectDirective);
65
+
20
66
  const UIStrings = {
21
67
  /**
22
68
  * @description Title of the device dimensions selection item in the Device Mode Toolbar.
@@ -188,10 +234,226 @@ const UIStrings = {
188
234
  } as const;
189
235
  const str_ = i18n.i18n.registerUIStrings('panels/emulation/DeviceModeToolbar.ts', UIStrings);
190
236
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
191
- const {ifDefined, live, styleMap} = Directives;
237
+ const {ifDefined, live} = Directives;
192
238
  const {widget} = UI.Widget;
193
239
  const {bindToSetting} = UI.UIUtils;
194
240
 
241
+ export interface DeviceModeOption {
242
+ device: EmulationModel.EmulatedDevices.EmulatedDevice;
243
+ title: string;
244
+ selected: boolean;
245
+ jslogContext: string;
246
+ }
247
+
248
+ export interface ViewInput {
249
+ isResponsive: boolean;
250
+ isFullHeight: boolean;
251
+ widthValue: string;
252
+ heightValue: string;
253
+ heightPlaceholder: string;
254
+ modeButtonTitle: string;
255
+ modeButtonDisabled: boolean;
256
+ showSpanButton: boolean;
257
+ showPostureItem: boolean;
258
+ deviceModeOptions: {
259
+ responsive: {title: string, selected: boolean, jslogContext: string},
260
+ standard: DeviceModeOption[],
261
+ custom: DeviceModeOption[],
262
+ edit: {title: string, jslogContext: string},
263
+ };
264
+ scaleOptions: Array<{title: string, value: number, selected: boolean, jslogContext: string}>;
265
+ dprOptions: Array<{title: string, value: number, selected: boolean, jslogContext: string}>;
266
+ uaOptions: Array<{title: string, value: EmulationModel.DeviceModeModel.UA, selected: boolean, jslogContext: string}>;
267
+ postureOptions: Array<{title: string, value: string, selected: boolean}>;
268
+ selectedDeviceOption: {title: string, selected: boolean, jslogContext: string}|undefined;
269
+ deviceText: string;
270
+ scaleText: string;
271
+ dprText: string;
272
+ uaText: string;
273
+ postureText: string;
274
+ onDeviceChange: (event: Event) => void;
275
+ onWidthChange: (event: Event) => void;
276
+ onHeightChange: (event: Event) => void;
277
+ onScaleChange: (event: Event) => void;
278
+ onDeviceScaleChange: (event: Event) => void;
279
+ onUAChange: (event: Event) => void;
280
+ onPostureChange: (event: Event) => void;
281
+ onModeMenuClick: (event: Event) => void;
282
+ onSpanClick: () => void;
283
+ onMoreOptionsClick: (event: Event) => void;
284
+ autoAdjustScaleSetting: Common.Settings.Setting<boolean>;
285
+ showDeviceScaleFactorSetting: Common.Settings.Setting<boolean>;
286
+ showUserAgentTypeSetting: Common.Settings.Setting<boolean>;
287
+ }
288
+
289
+ export type View = (input: ViewInput, output: object, target: HTMLElement|DocumentFragment) => void;
290
+
291
+ function createSizeInput(
292
+ title: string, jslogContext: string, value: string|undefined, disabled: boolean|undefined,
293
+ placeholder: string|undefined, onChange: (event: Event) => void): LitTemplate {
294
+ return html`
295
+ <input type="number"
296
+ max=${EmulationModel.DeviceModeModel.MaxDeviceSize}
297
+ min=${EmulationModel.DeviceModeModel.MinDeviceSize}
298
+ title=${title}
299
+ class="device-mode-size-input"
300
+ .disabled=${disabled ?? false}
301
+ jslog=${VisualLogging.textField().track({change: true}).context(jslogContext)}
302
+ .value=${value ?? ''}
303
+ placeholder=${ifDefined(placeholder)}
304
+ @change=${onChange}
305
+ @keydown=${(event: Event): void => {
306
+ const input = event.target as HTMLInputElement;
307
+ let modifiedValue = UI.UIUtils.modifiedFloatNumber(Number(input.value), event);
308
+ if (modifiedValue === null) {
309
+ return;
310
+ }
311
+ modifiedValue = Math.min(modifiedValue, EmulationModel.DeviceModeModel.MaxDeviceSize);
312
+ modifiedValue = Math.max(modifiedValue, EmulationModel.DeviceModeModel.MinDeviceSize);
313
+
314
+ event.preventDefault();
315
+ input.value = String(modifiedValue);
316
+ input.dispatchEvent(new Event('change'));
317
+ }}>`;
318
+ }
319
+
320
+ export const DEFAULT_VIEW: View = (input, _output, target) => {
321
+ // clang-format off
322
+ render(html`
323
+ <devtools-toolbar class="main-toolbar">
324
+ <div class="device-mode-empty-toolbar-element"></div>
325
+ ${i18nTemplate(str_, UIStrings.dimensions, {PH1: html`
326
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
327
+ ${autoWidthSelect(input.deviceText)}
328
+ title=${i18nString(UIStrings.deviceType)}
329
+ aria-label=${i18nString(UIStrings.deviceType)}
330
+ @change=${input.onDeviceChange}
331
+ .value=${live(input.selectedDeviceOption === input.deviceModeOptions.responsive ? 'Responsive' : (input.selectedDeviceOption?.title || 'Responsive'))}
332
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device')}>
333
+ <option value="Responsive" ?selected=${input.deviceModeOptions.responsive.selected} jslog=${VisualLogging.item(input.deviceModeOptions.responsive.jslogContext).track({click: true})}>
334
+ ${input.deviceModeOptions.responsive.title}
335
+ </option>
336
+ ${input.deviceModeOptions.standard.length > 0 ? html`
337
+ <optgroup label="Standard">
338
+ ${input.deviceModeOptions.standard.map(o => html`<option value=${o.title} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
339
+ </optgroup>
340
+ ` : ''}
341
+ ${input.deviceModeOptions.custom.length > 0 ? html`
342
+ <optgroup label="Custom">
343
+ ${input.deviceModeOptions.custom.map(o => html`<option value=${o.title} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
344
+ </optgroup>
345
+ ` : ''}
346
+ <option value="Edit" jslog=${VisualLogging.item(input.deviceModeOptions.edit.jslogContext).track({click: true})}>
347
+ ${input.deviceModeOptions.edit.title}
348
+ </option>
349
+ </select>`})}
350
+
351
+ ${createSizeInput(i18nString(UIStrings.width), 'width', input.widthValue, !input.isResponsive, '', input.onWidthChange)}
352
+
353
+ <div class="device-mode-x">×</div>
354
+ ${createSizeInput(i18nString(UIStrings.heightLeaveEmptyForFull), 'height', input.heightValue, !input.isResponsive, input.heightPlaceholder, input.onHeightChange)}
355
+
356
+ <div class="device-mode-empty-toolbar-element"></div>
357
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
358
+ ${autoWidthSelect(input.scaleText)}
359
+ title=${i18nString(UIStrings.zoom)}
360
+ aria-label=${i18nString(UIStrings.zoom)}
361
+ @change=${input.onScaleChange}
362
+ .value=${String(input.scaleOptions.find(o => o.selected)?.value || '')}
363
+ jslog=${VisualLogging.dropDown().track({change: true}).context('scale')}>
364
+ ${input.scaleOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
365
+ </select>
366
+
367
+ <devtools-button .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'center-focus-weak',
368
+ toggledIconName: 'center-focus-weak', toggleType: Buttons.Button.ToggleType.PRIMARY} as Buttons.Button.ButtonData}
369
+ class="toolbar-button" title=${i18nString(UIStrings.autoadjustZoom)}
370
+ ${bindToSetting(input.autoAdjustScaleSetting)}>
371
+ </devtools-button>
372
+
373
+ <div class="device-mode-empty-toolbar-element"></div>
374
+
375
+ ${input.showDeviceScaleFactorSetting.get() ? html`
376
+ <div class="toolbar-text">
377
+ ${i18nTemplate(str_, UIStrings.dpr, {
378
+ PH1: html`
379
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
380
+ ${autoWidthSelect(input.dprText)}
381
+ title=${i18nString(UIStrings.devicePixelRatio)}
382
+ aria-label=${i18nString(UIStrings.devicePixelRatio)}
383
+ @change=${input.onDeviceScaleChange}
384
+ .value=${String(input.dprOptions.find(o => o.selected)?.value || '')}
385
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device-pixel-ratio')}
386
+ ?disabled=${!input.isResponsive}>
387
+ ${input.dprOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
388
+ </select>`
389
+ })}
390
+ </div>` : ''}
391
+
392
+ <div class="device-mode-empty-toolbar-element"></div>
393
+ ${input.showUserAgentTypeSetting.get() ? html`
394
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
395
+ ${autoWidthSelect(input.uaText)}
396
+ title=${i18nString(UIStrings.deviceType)}
397
+ aria-label=${i18nString(UIStrings.deviceType)}
398
+ @change=${input.onUAChange}
399
+ .value=${input.uaOptions.find(o => o.selected)?.value || ''}
400
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device-type')}
401
+ ?disabled=${!input.isResponsive}>
402
+ ${input.uaOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
403
+ </select>` : ''}
404
+ <select class="dark-text" ${widget(MobileThrottling.NetworkThrottlingSelector.NetworkThrottlingSelect, {
405
+ title: i18nString(UIStrings.throttling),
406
+ bindToGlobalConditions: true,
407
+ })}></select>
408
+ <select class="dark-text toolbar-has-dropdown-shrinkable" ${widget(
409
+ MobileThrottling.ThrottlingManager.SaveDataOverrideSelect)}></select>
410
+
411
+ <div class="device-mode-empty-toolbar-element"></div>
412
+ <devtools-button class="toolbar-button"
413
+ .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'screen-rotation'} as Buttons.Button.ButtonData}
414
+ jslog=${VisualLogging.action('screen-rotation').track({click: true})}
415
+ @click=${input.onModeMenuClick}
416
+ .title=${input.modeButtonTitle}
417
+ .disabled=${input.modeButtonDisabled}>
418
+ </devtools-button>
419
+
420
+ <!-- Show dual screen toolbar -->
421
+ ${input.showSpanButton ? html`
422
+ <devtools-button class="toolbar-button"
423
+ .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'device-fold'} as Buttons.Button.ButtonData}
424
+ jslog=${VisualLogging.action('device-fold').track({click: true})}
425
+ .title=${i18nString(UIStrings.toggleDualscreenMode)}
426
+ @click=${input.onSpanClick}>
427
+ </devtools-button>` : ''}
428
+
429
+ <!-- Show posture toolbar menu for foldable devices. -->
430
+ <div class="device-mode-empty-toolbar-element"></div>
431
+ ${input.showPostureItem ? html`
432
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
433
+ ${autoWidthSelect(input.postureText)}
434
+ title=${i18nString(UIStrings.devicePosture)}
435
+ aria-label=${i18nString(UIStrings.devicePosture)}
436
+ @change=${input.onPostureChange}
437
+ .value=${input.postureOptions.find(o => o.selected)?.value || ''}
438
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device-posture')}>
439
+ ${input.postureOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.value.toLowerCase()).track({click: true})}>${o.title}</option>`)}
440
+ </select>` : ''}
441
+ </devtools-toolbar>
442
+ <devtools-toolbar class="device-mode-toolbar-options" wrappable>
443
+ <div class="device-mode-empty-toolbar-element"></div>
444
+ <devtools-button
445
+ .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'dots-vertical', title: i18nString(UIStrings.moreOptions)} as Buttons.Button.ButtonData}
446
+ @click=${input.onMoreOptionsClick}
447
+ jslog=${VisualLogging.dropDown('more-options').track({click: true})}
448
+ ></devtools-button></devtools-toolbar>
449
+ `, target, {container: {
450
+ classes: ['device-mode-toolbar'],
451
+ attributes: {jslog: `${VisualLogging.toolbar('device-mode').track({resize: true})}`,
452
+ }
453
+ }});
454
+ // clang-format on
455
+ };
456
+
195
457
  export class DeviceModeToolbar extends UI.Widget.Widget {
196
458
  private model: EmulationModel.DeviceModeModel.DeviceModeModel;
197
459
  private readonly showMediaInspectorSetting: Common.Settings.Setting<boolean>;
@@ -203,16 +465,14 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
203
465
  private readonly lastMode: Map<EmulationModel.EmulatedDevices.EmulatedDevice, EmulationModel.EmulatedDevices.Mode>;
204
466
  private readonly emulatedDevicesList: EmulationModel.EmulatedDevices.EmulatedDevicesList;
205
467
  private readonly persistenceSetting: Common.Settings.Setting<{device: string, orientation: string, mode: string}>;
206
- private mainToolbar: UI.Toolbar.Toolbar;
207
- private optionsToolbar: UI.Toolbar.Toolbar;
208
- readonly #itemWidthCache = new Map<string, string>();
209
- #measuringElement: HTMLSelectElement|null = null;
468
+ private readonly view: View;
210
469
 
211
470
  constructor(
212
471
  model: EmulationModel.DeviceModeModel.DeviceModeModel,
213
- showMediaInspectorSetting: Common.Settings.Setting<boolean>,
214
- showRulersSetting: Common.Settings.Setting<boolean>) {
472
+ showMediaInspectorSetting: Common.Settings.Setting<boolean>, showRulersSetting: Common.Settings.Setting<boolean>,
473
+ view: View = DEFAULT_VIEW) {
215
474
  super();
475
+ this.view = view;
216
476
  this.model = model;
217
477
  this.showMediaInspectorSetting = showMediaInspectorSetting;
218
478
  this.showRulersSetting = showRulersSetting;
@@ -231,11 +491,6 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
231
491
 
232
492
  this.lastMode = new Map();
233
493
 
234
- this.contentElement.classList.add('device-mode-toolbar');
235
- this.contentElement.setAttribute('jslog', VisualLogging.toolbar('device-mode').track({resize: true}).toString());
236
- this.mainToolbar = this.createMainToolbar();
237
- this.optionsToolbar = this.createOptionsToolbar();
238
-
239
494
  this.emulatedDevicesList = EmulationModel.EmulatedDevices.EmulatedDevicesList.instance();
240
495
  this.emulatedDevicesList.addEventListener(
241
496
  EmulationModel.EmulatedDevices.Events.CUSTOM_DEVICES_UPDATED, this.deviceListChanged, this);
@@ -251,50 +506,10 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
251
506
  this.model.deviceScaleFactorSetting().addChangeListener(this.requestUpdate, this);
252
507
  this.model.addEventListener(EmulationModel.DeviceModeModel.Events.UPDATED, this.requestUpdate, this);
253
508
 
254
- this.requestUpdate();
509
+ this.performUpdate();
255
510
  }
256
511
 
257
- private createEmptyToolbarElement(): HTMLDivElement {
258
- const element = document.createElement('div');
259
- element.classList.add('device-mode-empty-toolbar-element');
260
- return element;
261
- }
262
-
263
- private createSizeInput(
264
- title: string, jslogContext: string, value: string|undefined, disabled: boolean|undefined,
265
- placeholder: string|undefined, onChange: (event: Event) => void): LitTemplate {
266
- return html`
267
- <input type="number"
268
- max=${EmulationModel.DeviceModeModel.MaxDeviceSize}
269
- min=${EmulationModel.DeviceModeModel.MinDeviceSize}
270
- title=${title}
271
- class="device-mode-size-input"
272
- .disabled=${disabled ?? false}
273
- jslog=${VisualLogging.textField().track({change: true}).context(jslogContext)}
274
- .value=${value ?? ''}
275
- placeholder=${ifDefined(placeholder)}
276
- @change=${onChange}
277
- @keydown=${(event: Event): void => {
278
- const input = event.target as HTMLInputElement;
279
- let modifiedValue = UI.UIUtils.modifiedFloatNumber(Number(input.value), event);
280
- if (modifiedValue === null) {
281
- return;
282
- }
283
- modifiedValue = Math.min(modifiedValue, EmulationModel.DeviceModeModel.MaxDeviceSize);
284
- modifiedValue = Math.max(modifiedValue, EmulationModel.DeviceModeModel.MinDeviceSize);
285
-
286
- event.preventDefault();
287
- input.value = String(modifiedValue);
288
- input.dispatchEvent(new Event('change'));
289
- }}>`;
290
- }
291
-
292
- private createMainToolbar(): UI.Toolbar.Toolbar {
293
- const mainToolbar = this.contentElement.createChild('devtools-toolbar', 'main-toolbar');
294
- return mainToolbar as UI.Toolbar.Toolbar;
295
- }
296
-
297
- private renderMainToolbar(): void {
512
+ override performUpdate(): void {
298
513
  const isResponsive = this.model.type() === EmulationModel.DeviceModeModel.Type.Responsive;
299
514
  const isFullHeight = isResponsive && this.model.isFullHeight();
300
515
  const size = this.model.appliedDeviceSize();
@@ -303,6 +518,24 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
303
518
  const heightPlaceholder = String(size.height);
304
519
 
305
520
  const device = this.model.device();
521
+
522
+ if (this.model.type() === EmulationModel.DeviceModeModel.Type.Device && device) {
523
+ this.lastMode.set(device, (this.model.mode() as EmulationModel.EmulatedDevices.Mode));
524
+ }
525
+
526
+ const value = this.persistenceSetting.get();
527
+ const currentMode = this.model.mode();
528
+ if (device) {
529
+ value.device = device.title;
530
+ value.orientation = currentMode ? currentMode.orientation : '';
531
+ value.mode = currentMode ? currentMode.title : '';
532
+ } else {
533
+ value.device = '';
534
+ value.orientation = '';
535
+ value.mode = '';
536
+ }
537
+ this.persistenceSetting.set(value);
538
+
306
539
  let modeButtonTitle = i18nString(UIStrings.rotate);
307
540
  let modeButtonDisabled = false;
308
541
  if (this.model.isScreenOrientationLocked()) {
@@ -348,149 +581,64 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
348
581
  const uaText = uaOptions.find(o => o.selected)?.title || '';
349
582
  const postureText = postureOptions.find(o => o.selected)?.title || '';
350
583
 
351
- // clang-format off
352
- render(html`
353
- <div class="device-mode-empty-toolbar-element"></div>
354
- ${i18nTemplate(str_, UIStrings.dimensions, {PH1: html`
355
- <select class="dark-text toolbar-has-dropdown-shrinkable"
356
- style=${styleMap({width: this.calculateItemWidth(deviceText)})}
357
- title=${i18nString(UIStrings.deviceType)}
358
- aria-label=${i18nString(UIStrings.deviceType)}
359
- @change=${this.onDeviceChange.bind(this)}
360
- .value=${live(selectedDeviceOption === deviceModeOptions.responsive ? 'Responsive' : (selectedDeviceOption?.title || 'Responsive'))}
361
- jslog=${VisualLogging.dropDown().track({change: true}).context('device')}>
362
- <option value="Responsive" ?selected=${deviceModeOptions.responsive.selected} jslog=${VisualLogging.item(deviceModeOptions.responsive.jslogContext).track({click: true})}>
363
- ${deviceModeOptions.responsive.title}
364
- </option>
365
- ${deviceModeOptions.standard.length > 0 ? html`
366
- <optgroup label="Standard">
367
- ${deviceModeOptions.standard.map(o => html`<option value=${o.title} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
368
- </optgroup>
369
- ` : ''}
370
- ${deviceModeOptions.custom.length > 0 ? html`
371
- <optgroup label="Custom">
372
- ${deviceModeOptions.custom.map(o => html`<option value=${o.title} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
373
- </optgroup>
374
- ` : ''}
375
- <option value="Edit" jslog=${VisualLogging.item(deviceModeOptions.edit.jslogContext).track({click: true})}>
376
- ${deviceModeOptions.edit.title}
377
- </option>
378
- </select>`})}
379
-
380
- ${this.createSizeInput(i18nString(UIStrings.width), 'width', widthValue, !isResponsive, '', (event: Event) => {
584
+ const enabled = this.model.toolbarControlsEnabledSetting().get();
585
+ this.contentElement.classList.toggle('disabled', !enabled);
586
+
587
+ const input: ViewInput = {
588
+ isResponsive,
589
+ isFullHeight,
590
+ widthValue,
591
+ heightValue,
592
+ heightPlaceholder,
593
+ modeButtonTitle,
594
+ modeButtonDisabled,
595
+ showSpanButton,
596
+ showPostureItem,
597
+ deviceModeOptions,
598
+ scaleOptions,
599
+ dprOptions,
600
+ uaOptions,
601
+ postureOptions,
602
+ selectedDeviceOption,
603
+ deviceText,
604
+ scaleText,
605
+ dprText,
606
+ uaText,
607
+ postureText,
608
+ onDeviceChange: this.onDeviceChange.bind(this),
609
+ onWidthChange: (event: Event) => {
381
610
  const width = Number((event.target as HTMLInputElement).value);
382
611
  if (this.autoAdjustScaleSetting.get()) {
383
612
  this.model.setWidthAndScaleToFit(width);
384
613
  } else {
385
614
  this.model.setWidth(width);
386
615
  }
387
- })}
388
-
389
- <div class="device-mode-x">×</div>
390
- ${this.createSizeInput(i18nString(UIStrings.heightLeaveEmptyForFull), 'height', heightValue, !isResponsive, heightPlaceholder, (event: Event) => {
616
+ },
617
+ onHeightChange: (event: Event) => {
391
618
  const height = Number((event.target as HTMLInputElement).value);
392
619
  if (this.autoAdjustScaleSetting.get()) {
393
620
  this.model.setHeightAndScaleToFit(height);
394
621
  } else {
395
622
  this.model.setHeight(height);
396
623
  }
397
- })}
398
-
399
- <div class="device-mode-empty-toolbar-element"></div>
400
- <select class="dark-text toolbar-has-dropdown-shrinkable"
401
- style=${styleMap({width: this.calculateItemWidth(scaleText)})}
402
- title=${i18nString(UIStrings.zoom)}
403
- aria-label=${i18nString(UIStrings.zoom)}
404
- @change=${this.onScaleChange.bind(this)}
405
- .value=${String(scaleOptions.find(o => o.selected)?.value || '')}
406
- jslog=${VisualLogging.dropDown().track({change: true}).context('scale')}>
407
- ${scaleOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
408
- </select>
409
-
410
- <devtools-button .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'center-focus-weak',
411
- toggledIconName: 'center-focus-weak', toggleType: Buttons.Button.ToggleType.PRIMARY} as Buttons.Button.ButtonData}
412
- class="toolbar-button" title=${i18nString(UIStrings.autoadjustZoom)}
413
- ${bindToSetting(this.autoAdjustScaleSetting)}>
414
- </devtools-button>
415
-
416
- <div class="device-mode-empty-toolbar-element"></div>
417
-
418
- ${this.showDeviceScaleFactorSetting.get() ? html`
419
- ${i18nTemplate(str_, UIStrings.dpr, {
420
- PH1: html`
421
- <select class="dark-text toolbar-has-dropdown-shrinkable"
422
- style=${styleMap({width: this.calculateItemWidth(dprText)})}
423
- title=${i18nString(UIStrings.devicePixelRatio)}
424
- aria-label=${i18nString(UIStrings.devicePixelRatio)}
425
- @change=${this.onDeviceScaleChange.bind(this)}
426
- .value=${String(dprOptions.find(o => o.selected)?.value || '')}
427
- jslog=${VisualLogging.dropDown().track({change: true}).context('device-pixel-ratio')}
428
- ?disabled=${!isResponsive}>
429
- ${dprOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
430
- </select>`
431
- })}` : ''}
432
-
433
- <div class="device-mode-empty-toolbar-element"></div>
434
- ${this.showUserAgentTypeSetting.get() ? html`
435
- <select class="dark-text toolbar-has-dropdown-shrinkable"
436
- style=${styleMap({width: this.calculateItemWidth(uaText)})}
437
- title=${i18nString(UIStrings.deviceType)}
438
- aria-label=${i18nString(UIStrings.deviceType)}
439
- @change=${this.onUAChange.bind(this)}
440
- .value=${uaOptions.find(o => o.selected)?.value || ''}
441
- jslog=${VisualLogging.dropDown().track({change: true}).context('device-type')}
442
- ?disabled=${!isResponsive}>
443
- ${uaOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.jslogContext).track({click: true})}>${o.title}</option>`)}
444
- </select>` : ''}
445
- <select class="dark-text" ${widget(MobileThrottling.NetworkThrottlingSelector.NetworkThrottlingSelect, {
446
- title: i18nString(UIStrings.throttling),
447
- bindToGlobalConditions: true,
448
- })}></select>
449
- <select class="dark-text toolbar-has-dropdown-shrinkable" ${widget(
450
- MobileThrottling.ThrottlingManager.SaveDataOverrideSelect)}></select>
451
-
452
- <div class="device-mode-empty-toolbar-element"></div>
453
- <devtools-button class="toolbar-button"
454
- .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'screen-rotation',
455
- disabled: modeButtonDisabled} as Buttons.Button.ButtonData}
456
- jslog=${VisualLogging.action('screen-rotation').track({click: true})}
457
- @click=${this.modeMenuClicked.bind(this)}
458
- .title=${modeButtonTitle}>
459
- </devtools-button>
460
-
461
- <!-- Show dual screen toolbar -->
462
- ${showSpanButton ? html`
463
- <devtools-button class="toolbar-button"
464
- .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'device-fold'} as Buttons.Button.ButtonData}
465
- jslog=${VisualLogging.action('device-fold').track({click: true})}
466
- .title=${i18nString(UIStrings.toggleDualscreenMode)}
467
- @click=${this.spanClicked.bind(this)}>
468
- </devtools-button>` : ''}
469
-
470
- <!-- Show posture toolbar menu for foldable devices. -->
471
- <div class="device-mode-empty-toolbar-element"></div>
472
- ${showPostureItem ? html`
473
- <select class="dark-text toolbar-has-dropdown-shrinkable"
474
- style=${styleMap({width: this.calculateItemWidth(postureText)})}
475
- title=${i18nString(UIStrings.devicePosture)}
476
- aria-label=${i18nString(UIStrings.devicePosture)}
477
- @change=${this.onPostureChange.bind(this)}
478
- .value=${postureOptions.find(o => o.selected)?.value || ''}
479
- jslog=${VisualLogging.dropDown().track({change: true}).context('device-posture')}>
480
- ${postureOptions.map(o => html`<option value=${o.value} ?selected=${o.selected} jslog=${VisualLogging.item(o.value.toLowerCase()).track({click: true})}>${o.title}</option>`)}
481
- </select>` : ''}`, this.mainToolbar, {host: this});
482
- // clang-format on
483
- }
624
+ },
625
+ onScaleChange: this.onScaleChange.bind(this),
626
+ onDeviceScaleChange: this.onDeviceScaleChange.bind(this),
627
+ onUAChange: this.onUAChange.bind(this),
628
+ onPostureChange: this.onPostureChange.bind(this),
629
+ onModeMenuClick: this.modeMenuClicked.bind(this),
630
+ onSpanClick: this.spanClicked.bind(this),
631
+ onMoreOptionsClick: (event: Event) => {
632
+ const contextMenu = new UI.ContextMenu.ContextMenu(event);
633
+ this.appendOptionsMenuItems(contextMenu);
634
+ void contextMenu.show();
635
+ },
636
+ autoAdjustScaleSetting: this.autoAdjustScaleSetting,
637
+ showDeviceScaleFactorSetting: this.showDeviceScaleFactorSetting,
638
+ showUserAgentTypeSetting: this.showUserAgentTypeSetting,
639
+ };
484
640
 
485
- private createOptionsToolbar(): UI.Toolbar.Toolbar {
486
- const optionsToolbar = this.contentElement.createChild('devtools-toolbar', 'device-mode-toolbar-options');
487
- optionsToolbar.wrappable = true;
488
- optionsToolbar.appendToolbarItem(new UI.Toolbar.ToolbarItem(this.createEmptyToolbarElement()));
489
- const moreOptionsButton = new UI.Toolbar.ToolbarMenuButton(
490
- this.appendOptionsMenuItems.bind(this), true, undefined, 'more-options', 'dots-vertical');
491
- moreOptionsButton.setTitle(i18nString(UIStrings.moreOptions));
492
- optionsToolbar.appendToolbarItem(moreOptionsButton);
493
- return optionsToolbar;
641
+ this.view(input, {}, this.contentElement);
494
642
  }
495
643
 
496
644
  private getDevicePostureOptions(): Array<{title: string, value: string, selected: boolean}> {
@@ -873,35 +1021,6 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
873
1021
  return `${(this.model.scale() * 100).toFixed(0)}`;
874
1022
  }
875
1023
 
876
- override performUpdate(): void {
877
- const enabled = this.model.toolbarControlsEnabledSetting().get();
878
- this.mainToolbar.setEnabled(enabled);
879
- this.optionsToolbar.setEnabled(enabled);
880
-
881
- const device = this.model.device();
882
-
883
- if (this.model.type() === EmulationModel.DeviceModeModel.Type.Device) {
884
- this.lastMode.set(
885
- (this.model.device() as EmulationModel.EmulatedDevices.EmulatedDevice),
886
- (this.model.mode() as EmulationModel.EmulatedDevices.Mode));
887
- }
888
-
889
- const value = this.persistenceSetting.get();
890
- const currentMode = this.model.mode();
891
- if (device) {
892
- value.device = device.title;
893
- value.orientation = currentMode ? currentMode.orientation : '';
894
- value.mode = currentMode ? currentMode.title : '';
895
- } else {
896
- value.device = '';
897
- value.orientation = '';
898
- value.mode = '';
899
- }
900
- this.persistenceSetting.set(value);
901
-
902
- this.renderMainToolbar();
903
- }
904
-
905
1024
  restore(): void {
906
1025
  for (const device of this.allDevices()) {
907
1026
  if (device.title === this.persistenceSetting.get().device) {
@@ -918,36 +1037,4 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
918
1037
 
919
1038
  this.model.emulate(EmulationModel.DeviceModeModel.Type.Responsive, null, null);
920
1039
  }
921
-
922
- private calculateItemWidth(text: string): string {
923
- if (!text) {
924
- return '';
925
- }
926
- if (this.#itemWidthCache.has(text)) {
927
- return this.#itemWidthCache.get(text) as string;
928
- }
929
-
930
- if (!this.#measuringElement) {
931
- this.#measuringElement = document.createElement('select');
932
- this.#measuringElement.className = 'dark-text toolbar-has-dropdown-shrinkable';
933
- this.#measuringElement.style.width = 'fit-content';
934
- this.#measuringElement.style.position = 'absolute';
935
- this.#measuringElement.style.visibility = 'hidden';
936
- this.#measuringElement.style.pointerEvents = 'none';
937
- const dummyOption = document.createElement('option');
938
- this.#measuringElement.appendChild(dummyOption);
939
- this.contentElement.appendChild(this.#measuringElement);
940
- }
941
-
942
- const dummyOption = this.#measuringElement.options[0];
943
- dummyOption.textContent = text;
944
-
945
- const width = this.#measuringElement.offsetWidth;
946
-
947
- const widthPx = width ? `${width}px` : '';
948
- if (width > 0) {
949
- this.#itemWidthCache.set(text, widthPx);
950
- }
951
- return widthPx;
952
- }
953
1040
  }