chrome-devtools-frontend 1.0.1537268 → 1.0.1537860

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 (76) hide show
  1. package/.env.template +9 -0
  2. package/docs/get_the_code.md +27 -0
  3. package/front_end/core/common/SettingRegistration.ts +10 -7
  4. package/front_end/core/common/Settings.ts +3 -0
  5. package/front_end/core/sdk/sdk-meta.ts +8 -2
  6. package/front_end/entrypoints/inspector_main/RenderingOptions.ts +4 -3
  7. package/front_end/generated/SupportedCSSProperties.js +1 -0
  8. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +23 -7
  9. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +110 -5
  10. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +50 -45
  11. package/front_end/models/cpu_profile/ProfileTreeModel.ts +7 -7
  12. package/front_end/models/trace_source_maps_resolver/SourceMapsResolver.ts +1 -1
  13. package/front_end/panels/application/StorageView.ts +3 -2
  14. package/front_end/panels/application/components/BackForwardCacheView.ts +34 -51
  15. package/front_end/panels/application/components/OriginTrialTreeView.ts +83 -80
  16. package/front_end/panels/application/components/backForwardCacheView.css +4 -0
  17. package/front_end/panels/application/components/badge.css +8 -0
  18. package/front_end/panels/application/preloading/components/PreloadingGrid.ts +2 -2
  19. package/front_end/panels/application/preloading/components/PreloadingString.ts +27 -0
  20. package/front_end/panels/autofill/AutofillView.ts +1 -1
  21. package/front_end/panels/console/ConsoleView.ts +11 -9
  22. package/front_end/panels/coverage/CoverageView.ts +1 -2
  23. package/front_end/panels/css_overview/CSSOverviewSidebarPanel.ts +1 -1
  24. package/front_end/panels/developer_resources/DeveloperResourcesView.ts +1 -1
  25. package/front_end/panels/elements/ElementStatePaneWidget.ts +1 -1
  26. package/front_end/panels/elements/EventListenersWidget.ts +1 -2
  27. package/front_end/panels/elements/PropertiesWidget.ts +1 -1
  28. package/front_end/panels/network/NetworkConfigView.ts +2 -1
  29. package/front_end/panels/network/NetworkPanel.ts +5 -4
  30. package/front_end/panels/network/RequestCookiesView.ts +2 -1
  31. package/front_end/panels/profiler/HeapSnapshotView.ts +3 -2
  32. package/front_end/panels/sensors/SensorsView.ts +4 -3
  33. package/front_end/panels/settings/FrameworkIgnoreListSettingsTab.ts +8 -6
  34. package/front_end/panels/settings/KeybindsSettingsTab.ts +3 -2
  35. package/front_end/panels/settings/SettingsScreen.ts +2 -1
  36. package/front_end/panels/settings/WorkspaceSettingsTab.ts +1 -1
  37. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +2 -1
  38. package/front_end/panels/sources/SourcesPanel.ts +2 -1
  39. package/front_end/panels/sources/sources-meta.ts +8 -1
  40. package/front_end/panels/timeline/TimelinePanel.ts +4 -3
  41. package/front_end/panels/timeline/TimelineUIUtils.ts +4 -20
  42. package/front_end/panels/timeline/components/LiveMetricsView.ts +1 -0
  43. package/front_end/panels/timeline/components/SidebarAnnotationsTab.ts +2 -0
  44. package/front_end/third_party/chromium/README.chromium +1 -1
  45. package/front_end/ui/components/adorners/Adorner.ts +2 -1
  46. package/front_end/ui/components/buttons/Button.docs.ts +195 -0
  47. package/front_end/ui/components/settings/SettingCheckbox.ts +49 -14
  48. package/front_end/ui/components/settings/settingCheckbox.css +6 -1
  49. package/front_end/ui/components/spinners/Spinners.docs.ts +13 -0
  50. package/front_end/ui/components/tooltips/Tooltip.docs.ts +76 -0
  51. package/front_end/ui/legacy/FilterBar.ts +1 -2
  52. package/front_end/ui/legacy/RadioButton.docs.ts +41 -0
  53. package/front_end/ui/legacy/SelectMenu.docs.ts +98 -0
  54. package/front_end/ui/legacy/Toolbar.ts +4 -6
  55. package/front_end/ui/legacy/UIUtils.ts +114 -1
  56. package/front_end/ui/legacy/Widget.ts +62 -34
  57. package/front_end/ui/legacy/components/settings_ui/SettingsUI.ts +125 -0
  58. package/front_end/ui/legacy/components/settings_ui/settings_ui.ts +8 -0
  59. package/front_end/ui/legacy/legacy.ts +0 -2
  60. package/front_end/ui/visual_logging/KnownContextValues.ts +3 -0
  61. package/package.json +1 -1
  62. package/front_end/models/trace/lantern/testing/MetricTestUtils.ts +0 -62
  63. package/front_end/models/trace/lantern/testing/testing.ts +0 -5
  64. package/front_end/ui/components/docs/button/basic.html +0 -44
  65. package/front_end/ui/components/docs/button/basic.ts +0 -175
  66. package/front_end/ui/components/docs/radio_button/basic.html +0 -23
  67. package/front_end/ui/components/docs/radio_button/basic.ts +0 -50
  68. package/front_end/ui/components/docs/select_menu/basic.html +0 -19
  69. package/front_end/ui/components/docs/select_menu/basic.ts +0 -95
  70. package/front_end/ui/components/docs/select_menu/wide-option.html +0 -38
  71. package/front_end/ui/components/docs/select_menu/wide-option.ts +0 -43
  72. package/front_end/ui/components/docs/spinners/basic.html +0 -17
  73. package/front_end/ui/components/docs/spinners/basic.ts +0 -22
  74. package/front_end/ui/components/docs/tooltip/basic.html +0 -20
  75. package/front_end/ui/components/docs/tooltip/basic.ts +0 -82
  76. package/front_end/ui/legacy/SettingsUI.ts +0 -240
@@ -0,0 +1,13 @@
1
+ // Copyright 2025 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import './spinners.js';
6
+
7
+ import * as Lit from '../../lit/lit.js';
8
+
9
+ const {html} = Lit;
10
+
11
+ export async function render(container: HTMLElement) {
12
+ Lit.render(html`<devtools-spinner></devtools-spinner>`, container);
13
+ }
@@ -0,0 +1,76 @@
1
+ // Copyright 2025 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import * as Lit from '../../lit/lit.js';
6
+
7
+ import {Tooltip} from './Tooltip.js';
8
+
9
+ const {html} = Lit;
10
+
11
+ export async function render(container: HTMLElement) {
12
+ Lit.render(
13
+ html`
14
+ <div style="position: relative; z-index: 0;">
15
+ <button aria-describedby="simple-tooltip" style="position: absolute; left: 16px; top: 16px;">
16
+ Simple
17
+ </button>
18
+ <devtools-tooltip id="simple-tooltip">Simple content</devtools-tooltip>
19
+ </div>
20
+ <div style="position: relative; z-index: 0;">
21
+ <span
22
+ aria-details="rich-tooltip"
23
+ style="position: absolute; left: 16px; top: 116px; border: 1px solid black;"
24
+ >
25
+ Non-button click trigger
26
+ </span>
27
+ <devtools-tooltip id="rich-tooltip" variant="rich" use-click>
28
+ <p>Rich tooltip</p>
29
+ <button>Action</button>
30
+ </devtools-tooltip>
31
+ </div>
32
+ <div style="position: relative; z-index: 0;">
33
+ <button
34
+ id="removable"
35
+ @click=${() => document.getElementById('removable')?.remove()}
36
+ class="anchor"
37
+ aria-details="programatic"
38
+ style="position: absolute; left: 16px; top: 216px;"
39
+ >
40
+ Click to remove anchor
41
+ </button>
42
+ </div>
43
+ `,
44
+ container);
45
+
46
+ const anchor = container.querySelector('.anchor') as HTMLElement;
47
+ const programmaticTooltip = new Tooltip({id: 'programatic', variant: 'rich', anchor});
48
+ programmaticTooltip.append('Text content');
49
+ anchor.insertAdjacentElement('afterend', programmaticTooltip);
50
+
51
+ // Make the buttons draggable, so that we can experiment with the position of the tooltip.
52
+ container.querySelectorAll('button,span').forEach(anchor => draggable(anchor as HTMLElement));
53
+ function draggable(element: HTMLElement|null): void {
54
+ if (!element) {
55
+ return;
56
+ }
57
+ element.addEventListener('mousedown', event => {
58
+ const target = event.target as HTMLElement;
59
+ const offsetX = event.clientX - target.getBoundingClientRect().left + container.getBoundingClientRect().left;
60
+ const offsetY = event.clientY - target.getBoundingClientRect().top + container.getBoundingClientRect().top;
61
+
62
+ function onMouseMove(event: MouseEvent) {
63
+ target.style.left = `${event.clientX - offsetX}px`;
64
+ target.style.top = `${event.clientY - offsetY}px`;
65
+ }
66
+
67
+ function onMouseUp() {
68
+ document.removeEventListener('mousemove', onMouseMove);
69
+ document.removeEventListener('mouseup', onMouseUp);
70
+ }
71
+
72
+ document.addEventListener('mousemove', onMouseMove);
73
+ document.addEventListener('mouseup', onMouseUp);
74
+ });
75
+ }
76
+ }
@@ -15,11 +15,10 @@ import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
15
15
  import * as ARIAUtils from './ARIAUtils.js';
16
16
  import filterStyles from './filter.css.js';
17
17
  import {KeyboardShortcut, Modifiers} from './KeyboardShortcut.js';
18
- import {bindCheckbox} from './SettingsUI.js';
19
18
  import type {Suggestions} from './SuggestBox.js';
20
19
  import {type ToolbarButton, ToolbarFilter, ToolbarInput, ToolbarSettingToggle} from './Toolbar.js';
21
20
  import {Tooltip} from './Tooltip.js';
22
- import {CheckboxLabel, createTextChild} from './UIUtils.js';
21
+ import {bindCheckbox, CheckboxLabel, createTextChild} from './UIUtils.js';
23
22
  import {HBox} from './Widget.js';
24
23
 
25
24
  const UIStrings = {
@@ -0,0 +1,41 @@
1
+ // Copyright 2024 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import {UIUtils} from './legacy.js';
6
+
7
+ export async function render(container: HTMLElement) {
8
+ const styleElement = document.createElement('style');
9
+ styleElement.textContent = 'fieldset { label { display: block; } }';
10
+ container.appendChild(styleElement);
11
+
12
+ function radioExample({name, tabbable, disabled}: {
13
+ name: string,
14
+ tabbable: boolean,
15
+ disabled: boolean,
16
+ }): HTMLElement {
17
+ const example = document.createElement('fieldset');
18
+ example.style.marginTop = '20px';
19
+ const legend = document.createElement('legend');
20
+ legend.textContent = name;
21
+ const list = document.createElement('div');
22
+ for (let i = 0; i < 3; ++i) {
23
+ const {label, radio} = UIUtils.createRadioButton(name, `Option #${i + 1}`, name);
24
+ radio.tabIndex = tabbable ? 0 : -1;
25
+ radio.disabled = disabled;
26
+ radio.checked = i === 0;
27
+ list.append(label);
28
+ }
29
+ example.append(legend, list);
30
+ return example;
31
+ }
32
+
33
+ // Basic
34
+ container.appendChild(radioExample({name: 'basic', tabbable: true, disabled: false}));
35
+
36
+ // Not tab reachable
37
+ container.appendChild(radioExample({name: 'not-table-reachable', tabbable: false, disabled: false}));
38
+
39
+ // Disabled
40
+ container.appendChild(radioExample({name: 'disabled', tabbable: true, disabled: true}));
41
+ }
@@ -0,0 +1,98 @@
1
+ // Copyright 2023 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import * as Lit from '../lit/lit.js';
6
+ import * as VisualLogging from '../visual_logging/visual_logging.js';
7
+
8
+ import {UIUtils} from './legacy.js';
9
+
10
+ const {html} = Lit;
11
+
12
+ export async function render(container: HTMLElement) {
13
+ function createDivWithP(text: string): HTMLDivElement {
14
+ const div = document.createElement('div');
15
+ div.style.paddingLeft = '25px';
16
+ const p = document.createElement('p');
17
+ p.style.marginLeft = '-25px';
18
+ p.textContent = text;
19
+ div.appendChild(p);
20
+ container.appendChild(div);
21
+ return div;
22
+ }
23
+
24
+ function onChange(event: Event): void {
25
+ const menu = event.target;
26
+ if (menu instanceof HTMLSelectElement) {
27
+ // eslint-disable-next-line no-console
28
+ console.log('Option selected: ', menu.value);
29
+ }
30
+ }
31
+
32
+ {
33
+ const simpleMenuHTML = createDivWithP('Simple item select with lit-html');
34
+ Lit.render(
35
+ html`<select id="menu" aria-label="Select an option"
36
+ @change=${onChange}>
37
+ <option hidden>Select an option</option>
38
+ <option id="option-1" jslog=${VisualLogging.item('option-1').track({
39
+ click: true
40
+ })}
41
+ value="Option1">Option 1</option>
42
+ <option jslog=${VisualLogging.item('option-2').track({
43
+ click: true
44
+ })}
45
+ value="Option2">Option 2</option>
46
+ <option disabled jslog=${VisualLogging.item('option-3').track({
47
+ click: true
48
+ })}
49
+ value="Option3">Option 3</option>
50
+ </select>`,
51
+ simpleMenuHTML);
52
+ }
53
+
54
+ {
55
+ const groupMenuHTML = createDivWithP('Select with groups with lit-html');
56
+ Lit.render(
57
+ html`<select aria-label="Select an option"
58
+ @change=${onChange}>
59
+ <optgroup label="Group 1">
60
+ <option jslog=${VisualLogging.item('option-1').track({
61
+ click: true
62
+ })}
63
+ value="Option1">Option 1</option>
64
+ </optgroup>
65
+ <optgroup label="Group 2">
66
+ <option jslog=${VisualLogging.item('option-2').track({
67
+ click: true
68
+ })}
69
+ value="Option2">Option 2</option>
70
+ <option jslog=${VisualLogging.item('option-3').track({
71
+ click: true
72
+ })}
73
+ value="Option3">Option 3</option>
74
+ </optgroup>
75
+ </select>`,
76
+ groupMenuHTML);
77
+ }
78
+
79
+ {
80
+ const simpleMenuImperative = createDivWithP('Simple item select with imperative API');
81
+ const simpleSelect = UIUtils.createSelect('Select an option', [
82
+ 'Option 1',
83
+ 'Option 2',
84
+ 'Option 3',
85
+ ]);
86
+ simpleSelect.addEventListener('change', event => onChange(event));
87
+ simpleMenuImperative.appendChild(simpleSelect);
88
+ }
89
+
90
+ {
91
+ const groupMenuImperative = createDivWithP('Select with groups with imperative API');
92
+ const group1 = new Map<string, string[]>([['Group 1', ['Option 1']]]);
93
+ const group2 = new Map<string, string[]>([['Group 2', ['Option 2', 'Option 3']]]);
94
+ const groupSelect = UIUtils.createSelect('Select an option', [group1, group2]);
95
+ groupSelect.addEventListener('change', event => onChange(event));
96
+ groupMenuImperative.appendChild(groupSelect);
97
+ }
98
+ }
@@ -10,7 +10,6 @@ import * as Platform from '../../core/platform/platform.js';
10
10
  import * as Root from '../../core/root/root.js';
11
11
  import * as Buttons from '../../ui/components/buttons/buttons.js';
12
12
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
13
- import type * as Adorners from '../components/adorners/adorners.js';
14
13
  import * as IconButton from '../components/icon_button/icon_button.js';
15
14
 
16
15
  import {type Action, Events as ActionEvents} from './ActionRegistration.js';
@@ -18,12 +17,11 @@ import {ActionRegistry} from './ActionRegistry.js';
18
17
  import * as ARIAUtils from './ARIAUtils.js';
19
18
  import {ContextMenu} from './ContextMenu.js';
20
19
  import {GlassPane, PointerEventsBehavior} from './GlassPane.js';
21
- import {bindCheckbox} from './SettingsUI.js';
22
20
  import type {Suggestion} from './SuggestBox.js';
23
21
  import {Events as TextPromptEvents, TextPrompt} from './TextPrompt.js';
24
22
  import toolbarStyles from './toolbar.css.js';
25
23
  import {Tooltip} from './Tooltip.js';
26
- import {CheckboxLabel, LongClickController} from './UIUtils.js';
24
+ import {bindCheckbox, CheckboxLabel, LongClickController} from './UIUtils.js';
27
25
 
28
26
  const UIStrings = {
29
27
  /**
@@ -643,7 +641,7 @@ export class ToolbarButton extends ToolbarItem<ToolbarButton.EventTypes, Buttons
643
641
  this.text = text;
644
642
  }
645
643
 
646
- setAdorner(adorner: Adorners.Adorner.Adorner): void {
644
+ setAdorner(adorner: HTMLElement): void {
647
645
  if (this.adorner) {
648
646
  this.adorner.replaceWith(adorner);
649
647
  } else {
@@ -971,7 +969,7 @@ export class ToolbarMenuButton extends ToolbarItem<ToolbarButton.EventTypes> {
971
969
  private textElement?: HTMLElement;
972
970
  private text?: string;
973
971
  private iconName?: string;
974
- private adorner?: Adorners.Adorner.Adorner;
972
+ private adorner?: HTMLElement;
975
973
  private readonly contextMenuHandler: (arg0: ContextMenu) => void;
976
974
  private readonly useSoftMenu: boolean;
977
975
  private readonly keepOpen: boolean;
@@ -1028,7 +1026,7 @@ export class ToolbarMenuButton extends ToolbarItem<ToolbarButton.EventTypes> {
1028
1026
  this.text = text;
1029
1027
  }
1030
1028
 
1031
- setAdorner(adorner: Adorners.Adorner.Adorner): void {
1029
+ setAdorner(adorner: HTMLElement): void {
1032
1030
  if (this.iconName) {
1033
1031
  return;
1034
1032
  }
@@ -36,7 +36,7 @@
36
36
 
37
37
  import './Toolbar.js';
38
38
 
39
- import type * as Common from '../../core/common/common.js';
39
+ import * as Common from '../../core/common/common.js';
40
40
  import * as Host from '../../core/host/host.js';
41
41
  import * as i18n from '../../core/i18n/i18n.js';
42
42
  import * as Platform from '../../core/platform/platform.js';
@@ -2353,3 +2353,116 @@ export function copyTextToClipboard(text: string, alert?: string): void {
2353
2353
  export function getDevToolsBoundingElement(): HTMLElement {
2354
2354
  return InspectorView.maybeGetInspectorViewInstance()?.element || document.body;
2355
2355
  }
2356
+
2357
+ /**
2358
+ * @deprecated Prefer {@link bindToSetting} as this function leaks the checkbox via the setting listener.
2359
+ */
2360
+ export const bindCheckbox = function(
2361
+ input: CheckboxLabel, setting: Common.Settings.Setting<boolean>, metric?: UserMetricOptions): void {
2362
+ const setValue = bindCheckboxImpl(input, setting.set.bind(setting), metric);
2363
+ setting.addChangeListener(event => setValue(event.data));
2364
+ setValue(setting.get());
2365
+ };
2366
+
2367
+ export const bindCheckboxImpl = function(
2368
+ input: CheckboxLabel, apply: (value: boolean) => void, metric?: UserMetricOptions): (value: boolean) => void {
2369
+ input.addEventListener('change', onInputChanged, false);
2370
+
2371
+ function onInputChanged(): void {
2372
+ apply(input.checked);
2373
+
2374
+ if (input.checked && metric?.enable) {
2375
+ Host.userMetrics.actionTaken(metric.enable);
2376
+ }
2377
+
2378
+ if (!input.checked && metric?.disable) {
2379
+ Host.userMetrics.actionTaken(metric.disable);
2380
+ }
2381
+
2382
+ if (metric?.toggle) {
2383
+ Host.userMetrics.actionTaken(metric.toggle);
2384
+ }
2385
+ }
2386
+
2387
+ return function setValue(value: boolean): void {
2388
+ if (value !== input.checked) {
2389
+ input.checked = value;
2390
+ }
2391
+ };
2392
+ };
2393
+
2394
+ export const bindToSetting =
2395
+ (settingOrName: string|Common.Settings.Setting<boolean|string>|Common.Settings.RegExpSetting,
2396
+ stringValidator?: (newSettingValue: string) => boolean): ReturnType<typeof Directives.ref> => {
2397
+ const setting = typeof settingOrName === 'string' ?
2398
+ Common.Settings.Settings.instance().moduleSetting(settingOrName) :
2399
+ settingOrName;
2400
+
2401
+ // We can't use `setValue` as the change listener directly, otherwise we won't
2402
+ // be able to remove it again.
2403
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2404
+ let setValue: (value: any) => void;
2405
+ function settingChanged(): void {
2406
+ setValue(setting.get());
2407
+ }
2408
+
2409
+ if (setting.type() === Common.Settings.SettingType.BOOLEAN || typeof setting.defaultValue === 'boolean') {
2410
+ return Directives.ref(e => {
2411
+ if (e === undefined) {
2412
+ setting.removeChangeListener(settingChanged);
2413
+ return;
2414
+ }
2415
+
2416
+ setting.addChangeListener(settingChanged);
2417
+ setValue =
2418
+ bindCheckboxImpl(e as CheckboxLabel, (setting as Common.Settings.Setting<boolean>).set.bind(setting));
2419
+ setValue(setting.get());
2420
+ });
2421
+ }
2422
+
2423
+ if (setting.type() === Common.Settings.SettingType.REGEX || setting instanceof Common.Settings.RegExpSetting) {
2424
+ return Directives.ref(e => {
2425
+ if (e === undefined) {
2426
+ setting.removeChangeListener(settingChanged);
2427
+ return;
2428
+ }
2429
+
2430
+ setting.addChangeListener(settingChanged);
2431
+ setValue = bindInput(e as HTMLInputElement, setting.set.bind(setting), (value: string) => {
2432
+ try {
2433
+ new RegExp(value);
2434
+ return true;
2435
+ } catch {
2436
+ return false;
2437
+ }
2438
+ }, /* numeric */ false);
2439
+ setValue(setting.get());
2440
+ });
2441
+ }
2442
+
2443
+ if (typeof setting.defaultValue === 'string') {
2444
+ return Directives.ref(e => {
2445
+ if (e === undefined) {
2446
+ setting.removeChangeListener(settingChanged);
2447
+ return;
2448
+ }
2449
+
2450
+ setting.addChangeListener(settingChanged);
2451
+ setValue = bindInput(
2452
+ e as HTMLInputElement, setting.set.bind(setting), stringValidator ?? (() => true), /* numeric */ false);
2453
+ setValue(setting.get());
2454
+ });
2455
+ }
2456
+
2457
+ throw new Error(`Cannot infer type for setting '${setting.name}'`);
2458
+ };
2459
+
2460
+ /**
2461
+ * Track toggle action as a whole or
2462
+ * track on and off action separately.
2463
+ */
2464
+ export interface UserMetricOptions {
2465
+ toggle?: Host.UserMetrics.Action;
2466
+ enable?: Host.UserMetrics.Action;
2467
+ disable?: Host.UserMetrics.Action;
2468
+ }
@@ -68,6 +68,64 @@ export function widgetConfig<F extends WidgetFactory<Widget>, ParamKeys extends
68
68
  return new WidgetConfig(widgetClass, widgetParams);
69
69
  }
70
70
 
71
+ let currentUpdateQueue: Map<Widget, PromiseWithResolvers<void>>|null = null;
72
+ const currentlyProcessed = new Set<Widget>();
73
+ let nextUpdateQueue = new Map<Widget, PromiseWithResolvers<void>>();
74
+ let pendingAnimationFrame: number|null = null;
75
+
76
+ function enqueueIntoNextUpdateQueue(widget: Widget): Promise<void> {
77
+ const scheduledUpdate = nextUpdateQueue.get(widget) ?? Promise.withResolvers<void>();
78
+ nextUpdateQueue.delete(widget);
79
+ nextUpdateQueue.set(widget, scheduledUpdate);
80
+ if (pendingAnimationFrame === null) {
81
+ pendingAnimationFrame = requestAnimationFrame(runNextUpdate);
82
+ }
83
+ return scheduledUpdate.promise;
84
+ }
85
+
86
+ function enqueueWidgetUpdate(widget: Widget): Promise<void> {
87
+ if (currentUpdateQueue) {
88
+ if (currentlyProcessed.has(widget)) {
89
+ return enqueueIntoNextUpdateQueue(widget);
90
+ }
91
+ const scheduledUpdate = currentUpdateQueue.get(widget) ?? Promise.withResolvers<void>();
92
+ currentUpdateQueue.delete(widget);
93
+ currentUpdateQueue.set(widget, scheduledUpdate);
94
+ return scheduledUpdate.promise;
95
+ }
96
+ return enqueueIntoNextUpdateQueue(widget);
97
+ }
98
+
99
+ function cancelUpdate(widget: Widget): void {
100
+ if (currentUpdateQueue) {
101
+ const scheduledUpdate = currentUpdateQueue.get(widget);
102
+ if (scheduledUpdate) {
103
+ scheduledUpdate.resolve();
104
+ currentUpdateQueue.delete(widget);
105
+ }
106
+ }
107
+ const scheduledUpdate = nextUpdateQueue.get(widget);
108
+ if (scheduledUpdate) {
109
+ scheduledUpdate.resolve();
110
+ nextUpdateQueue.delete(widget);
111
+ }
112
+ }
113
+
114
+ function runNextUpdate(): void {
115
+ pendingAnimationFrame = null;
116
+ currentUpdateQueue = nextUpdateQueue;
117
+ nextUpdateQueue = new Map();
118
+ for (const [widget, {resolve}] of currentUpdateQueue) {
119
+ currentlyProcessed.add(widget);
120
+ void (async () => {
121
+ await widget.performUpdate();
122
+ resolve();
123
+ })();
124
+ }
125
+ currentUpdateQueue = null;
126
+ currentlyProcessed.clear();
127
+ }
128
+
71
129
  export class WidgetElement<WidgetT extends Widget> extends HTMLElement {
72
130
  #widgetClass?: WidgetFactory<WidgetT>;
73
131
  #widgetParams?: Partial<WidgetT>;
@@ -218,8 +276,7 @@ function decrementWidgetCounter(parentElement: Element, childElement: Element):
218
276
  // The resolved `updateComplete` promise, which is used as a marker for the
219
277
  // Widget's `#updateComplete` private property to indicate that there's no
220
278
  // pending update.
221
- const UPDATE_COMPLETE = Promise.resolve(true);
222
- const UPDATE_COMPLETE_RESOLVE = (_result: boolean): void => {};
279
+ const UPDATE_COMPLETE = Promise.resolve();
223
280
 
224
281
  /**
225
282
  * Additional options passed to the `Widget` constructor to configure the
@@ -277,8 +334,6 @@ export class Widget {
277
334
  #invalidationsRequested?: boolean;
278
335
  #externallyManaged?: boolean;
279
336
  #updateComplete = UPDATE_COMPLETE;
280
- #updateCompleteResolve = UPDATE_COMPLETE_RESOLVE;
281
- #updateRequestID = 0;
282
337
 
283
338
  /**
284
339
  * Constructs a new `Widget` with the given `options`.
@@ -617,14 +672,7 @@ export class Widget {
617
672
  return;
618
673
  }
619
674
 
620
- // Cancel any pending update.
621
- if (this.#updateRequestID !== 0) {
622
- cancelAnimationFrame(this.#updateRequestID);
623
- this.#updateCompleteResolve(true);
624
- this.#updateCompleteResolve = UPDATE_COMPLETE_RESOLVE;
625
- this.#updateComplete = UPDATE_COMPLETE;
626
- this.#updateRequestID = 0;
627
- }
675
+ cancelUpdate(this);
628
676
 
629
677
  // hideOnDetach means that we should never remove element from dom - content
630
678
  // has iframes and detaching it will hurt.
@@ -876,21 +924,6 @@ export class Widget {
876
924
  performUpdate(): Promise<void>|void {
877
925
  }
878
926
 
879
- async #performUpdateCallback(): Promise<boolean> {
880
- // Mark this update cycle as complete by assigning
881
- // the marker sentinel.
882
- this.#updateComplete = UPDATE_COMPLETE;
883
- this.#updateCompleteResolve = UPDATE_COMPLETE_RESOLVE;
884
- this.#updateRequestID = 0;
885
-
886
- // Run the actual update logic.
887
- await this.performUpdate();
888
-
889
- // Resolve the `updateComplete` with `true` if no
890
- // new update was triggered during this cycle.
891
- return this.#updateComplete === UPDATE_COMPLETE;
892
- }
893
-
894
927
  /**
895
928
  * Schedules an asynchronous update for this widget.
896
929
  *
@@ -898,12 +931,7 @@ export class Widget {
898
931
  * frame.
899
932
  */
900
933
  requestUpdate(): void {
901
- if (this.#updateComplete === UPDATE_COMPLETE) {
902
- this.#updateComplete = new Promise((resolve, reject) => {
903
- this.#updateCompleteResolve = resolve;
904
- this.#updateRequestID = requestAnimationFrame(() => this.#performUpdateCallback().then(resolve, reject));
905
- });
906
- }
934
+ this.#updateComplete = enqueueWidgetUpdate(this);
907
935
  }
908
936
 
909
937
  /**
@@ -931,7 +959,7 @@ export class Widget {
931
959
  * updating, the value is `true` if there are no more pending updates,
932
960
  * and `false` if the update cycle triggered another update.
933
961
  */
934
- get updateComplete(): Promise<boolean> {
962
+ get updateComplete(): Promise<void> {
935
963
  return this.#updateComplete;
936
964
  }
937
965
  }
@@ -0,0 +1,125 @@
1
+ // Copyright 2014 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ /* eslint-disable @devtools/no-imperative-dom-api */
6
+
7
+ import * as Common from '../../../../core/common/common.js';
8
+ import * as i18n from '../../../../core/i18n/i18n.js';
9
+ import * as Platform from '../../../../core/platform/platform.js';
10
+ import * as Settings from '../../../components/settings/settings.js';
11
+ import * as VisualLogging from '../../../visual_logging/visual_logging.js';
12
+ import * as UI from '../../legacy.js';
13
+
14
+ const UIStrings = {
15
+ /**
16
+ * @description Note when a setting change will require the user to reload DevTools
17
+ */
18
+ srequiresReload: '*Requires reload',
19
+ /**
20
+ * @description Message to display if a setting change requires a reload of DevTools
21
+ */
22
+ oneOrMoreSettingsHaveChanged: 'One or more settings have changed which requires a reload to take effect',
23
+ } as const;
24
+ const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/settings_ui/SettingsUI.ts', UIStrings);
25
+ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
26
+
27
+ export function createSettingCheckbox(
28
+ name: Common.UIString.LocalizedString, setting: Common.Settings.Setting<boolean>,
29
+ tooltip?: string): UI.UIUtils.CheckboxLabel {
30
+ const label = UI.UIUtils.CheckboxLabel.create(name, undefined, undefined, setting.name);
31
+ label.name = name;
32
+ UI.UIUtils.bindCheckbox(label, setting);
33
+ if (tooltip) {
34
+ UI.Tooltip.Tooltip.install(label, tooltip);
35
+ }
36
+ return label;
37
+ }
38
+
39
+ const createSettingSelect = function(
40
+ name: string, options: Common.Settings.SimpleSettingOption[], requiresReload: boolean|null,
41
+ setting: Common.Settings.Setting<unknown>, subtitle?: string): HTMLElement {
42
+ const container = document.createElement('div');
43
+ const settingSelectElement = container.createChild('p');
44
+ settingSelectElement.classList.add('settings-select');
45
+ const label = settingSelectElement.createChild('label');
46
+ const select = settingSelectElement.createChild('select');
47
+ label.textContent = name;
48
+ if (subtitle) {
49
+ container.classList.add('chrome-select-label');
50
+ label.createChild('p').textContent = subtitle;
51
+ }
52
+ select.setAttribute('jslog', `${VisualLogging.dropDown().track({change: true}).context(setting.name)}`);
53
+ UI.ARIAUtils.bindLabelToControl(label, select);
54
+
55
+ for (const option of options) {
56
+ if (option.text && typeof option.value === 'string') {
57
+ select.add(
58
+ UI.UIUtils.createOption(option.text, option.value, Platform.StringUtilities.toKebabCase(option.value)));
59
+ }
60
+ }
61
+
62
+ let reloadWarning: HTMLElement|(Element | null) = (null as Element | null);
63
+ if (requiresReload) {
64
+ reloadWarning = container.createChild('p', 'reload-warning hidden');
65
+ reloadWarning.textContent = i18nString(UIStrings.srequiresReload);
66
+ UI.ARIAUtils.markAsAlert(reloadWarning);
67
+ }
68
+
69
+ const {deprecation} = setting;
70
+ if (deprecation) {
71
+ const warning = new Settings.SettingDeprecationWarning.SettingDeprecationWarning();
72
+ warning.data = deprecation;
73
+ label.appendChild(warning);
74
+ }
75
+
76
+ setting.addChangeListener(settingChanged);
77
+ settingChanged();
78
+ select.addEventListener('change', selectChanged, false);
79
+ return container;
80
+
81
+ function settingChanged(): void {
82
+ const newValue = setting.get();
83
+ for (let i = 0; i < options.length; i++) {
84
+ if (options[i].value === newValue) {
85
+ select.selectedIndex = i;
86
+ }
87
+ }
88
+ select.disabled = setting.disabled();
89
+ }
90
+
91
+ function selectChanged(): void {
92
+ // Don't use event.target.value to avoid conversion of the value to string.
93
+ setting.set(options[select.selectedIndex].value);
94
+ if (reloadWarning) {
95
+ reloadWarning.classList.remove('hidden');
96
+ UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning(
97
+ i18nString(UIStrings.oneOrMoreSettingsHaveChanged));
98
+ }
99
+ }
100
+ };
101
+
102
+ export const createControlForSetting = function(
103
+ setting: Common.Settings.Setting<unknown>, subtitle?: string): HTMLElement|null {
104
+ const uiTitle = setting.title();
105
+ switch (setting.type()) {
106
+ case Common.Settings.SettingType.BOOLEAN: {
107
+ const component = new Settings.SettingCheckbox.SettingCheckbox();
108
+ component.data = {
109
+ setting: setting as Common.Settings.Setting<boolean>,
110
+ };
111
+ component.onchange = () => {
112
+ if (setting.reloadRequired()) {
113
+ UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning(
114
+ i18nString(UIStrings.oneOrMoreSettingsHaveChanged));
115
+ }
116
+ };
117
+ return component;
118
+ }
119
+ case Common.Settings.SettingType.ENUM:
120
+ return createSettingSelect(uiTitle, setting.options(), setting.reloadRequired(), setting, subtitle);
121
+ default:
122
+ console.error('Invalid setting type: ' + setting.type());
123
+ return null;
124
+ }
125
+ };