chrome-devtools-frontend 1.0.1596535 → 1.0.1597448
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.
- package/agents/prompts/ui-widgets.md +7 -8
- package/docs/ui_engineering.md +10 -11
- package/front_end/core/host/InspectorFrontendHostAPI.ts +1 -0
- package/front_end/core/host/UserMetrics.ts +12 -0
- package/front_end/core/root/Runtime.ts +5 -0
- package/front_end/core/sdk/CPUThrottlingManager.ts +9 -12
- package/front_end/core/sdk/PageResourceLoader.ts +22 -1
- package/front_end/devtools_compatibility.js +2 -1
- package/front_end/models/ai_assistance/AiConversation.ts +5 -0
- package/front_end/models/ai_assistance/agents/AiAgent.ts +4 -0
- package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +24 -0
- package/front_end/models/ai_assistance/agents/StylingAgent.ts +289 -12
- package/front_end/models/greendev/Prototypes.ts +7 -1
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +35 -33
- package/front_end/panels/ai_assistance/PatchWidget.ts +6 -6
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +3 -28
- package/front_end/panels/ai_assistance/components/ChatView.ts +6 -11
- package/front_end/panels/ai_assistance/components/StylingAgentMarkdownRenderer.ts +200 -0
- package/front_end/panels/ai_assistance/components/chatMessage.css +0 -8
- package/front_end/panels/application/ServiceWorkerCacheViews.ts +1 -1
- package/front_end/panels/common/ExtensionServer.ts +15 -0
- package/front_end/panels/elements/ElementsTreeElement.ts +55 -47
- package/front_end/panels/elements/ElementsTreeOutline.ts +149 -28
- package/front_end/panels/lighthouse/LighthousePanel.ts +8 -0
- package/front_end/panels/settings/SettingsScreen.ts +3 -2
- package/front_end/ui/legacy/UIUtils.ts +5 -5
- package/front_end/ui/legacy/Widget.ts +33 -2
- package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
- package/package.json +1 -1
|
@@ -90,6 +90,12 @@ interface ViewInput {
|
|
|
90
90
|
omitRootDOMNode: boolean;
|
|
91
91
|
selectEnabled: boolean;
|
|
92
92
|
hideGutter: boolean;
|
|
93
|
+
maxTreeDepth?: number;
|
|
94
|
+
enableContextMenu?: boolean;
|
|
95
|
+
showComments?: boolean;
|
|
96
|
+
showAIButton?: boolean;
|
|
97
|
+
disableEdits?: boolean;
|
|
98
|
+
expandRoot?: boolean;
|
|
93
99
|
visibleWidth?: number;
|
|
94
100
|
visible?: boolean;
|
|
95
101
|
wrap: boolean;
|
|
@@ -119,7 +125,9 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
|
|
|
119
125
|
// FIXME: this is basically a ref to existing imperative
|
|
120
126
|
// implementation. Once this is declarative the ref should not be
|
|
121
127
|
// needed.
|
|
122
|
-
output.elementsTreeOutline = new ElementsTreeOutline(
|
|
128
|
+
output.elementsTreeOutline = new ElementsTreeOutline(
|
|
129
|
+
input.omitRootDOMNode, input.selectEnabled, input.hideGutter, input.maxTreeDepth, input.enableContextMenu,
|
|
130
|
+
input.showComments, input.showAIButton, input.disableEdits, input.expandRoot);
|
|
123
131
|
output.elementsTreeOutline.addEventListener(
|
|
124
132
|
ElementsTreeOutline.Events.SelectedNodeChanged, input.onSelectedNodeChanged, this);
|
|
125
133
|
output.elementsTreeOutline.addEventListener(
|
|
@@ -128,6 +136,7 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
|
|
|
128
136
|
output.elementsTreeOutline.addEventListener(UI.TreeOutline.Events.ElementCollapsed, input.onElementCollapsed, this);
|
|
129
137
|
target.appendChild(output.elementsTreeOutline.element);
|
|
130
138
|
}
|
|
139
|
+
|
|
131
140
|
if (input.visibleWidth !== undefined) {
|
|
132
141
|
output.elementsTreeOutline.setVisibleWidth(input.visibleWidth);
|
|
133
142
|
}
|
|
@@ -209,6 +218,11 @@ export class DOMTreeWidget extends UI.Widget.Widget {
|
|
|
209
218
|
onElementExpanded: () => void = () => {};
|
|
210
219
|
onElementCollapsed: () => void = () => {};
|
|
211
220
|
|
|
221
|
+
#maxTreeDepth?: number;
|
|
222
|
+
#enableContextMenu = true;
|
|
223
|
+
#showComments = true;
|
|
224
|
+
#showAIButton = true;
|
|
225
|
+
#disableEdits = false;
|
|
212
226
|
#visible = false;
|
|
213
227
|
#visibleWidth?: number;
|
|
214
228
|
#wrap = false;
|
|
@@ -232,6 +246,51 @@ export class DOMTreeWidget extends UI.Widget.Widget {
|
|
|
232
246
|
return this.#viewOutput.elementsTreeOutline?.rootDOMNode ?? null;
|
|
233
247
|
}
|
|
234
248
|
|
|
249
|
+
get maxTreeDepth(): number|undefined {
|
|
250
|
+
return this.#maxTreeDepth;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
set maxTreeDepth(maxTreeDepth: number|undefined) {
|
|
254
|
+
this.#maxTreeDepth = maxTreeDepth;
|
|
255
|
+
this.performUpdate();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
get enableContextMenu(): boolean {
|
|
259
|
+
return this.#enableContextMenu;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
set enableContextMenu(enableContextMenu: boolean) {
|
|
263
|
+
this.#enableContextMenu = enableContextMenu;
|
|
264
|
+
this.performUpdate();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get showComments(): boolean {
|
|
268
|
+
return this.#showComments;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
set showComments(showComments: boolean) {
|
|
272
|
+
this.#showComments = showComments;
|
|
273
|
+
this.performUpdate();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get showAIButton(): boolean {
|
|
277
|
+
return this.#showAIButton;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
set showAIButton(showAIButton: boolean) {
|
|
281
|
+
this.#showAIButton = showAIButton;
|
|
282
|
+
this.performUpdate();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
get disableEdits(): boolean {
|
|
286
|
+
return this.#disableEdits;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
set disableEdits(disableEdits: boolean) {
|
|
290
|
+
this.#disableEdits = disableEdits;
|
|
291
|
+
this.performUpdate();
|
|
292
|
+
}
|
|
293
|
+
|
|
235
294
|
#currentHighlightedNode: SDK.DOMModel.DOMNode|null = null;
|
|
236
295
|
|
|
237
296
|
#view: View;
|
|
@@ -323,6 +382,11 @@ export class DOMTreeWidget extends UI.Widget.Widget {
|
|
|
323
382
|
omitRootDOMNode: this.omitRootDOMNode,
|
|
324
383
|
selectEnabled: this.selectEnabled,
|
|
325
384
|
hideGutter: this.hideGutter,
|
|
385
|
+
maxTreeDepth: this.#maxTreeDepth,
|
|
386
|
+
enableContextMenu: this.#enableContextMenu,
|
|
387
|
+
showComments: this.#showComments,
|
|
388
|
+
showAIButton: this.#showAIButton,
|
|
389
|
+
disableEdits: this.#disableEdits,
|
|
326
390
|
visibleWidth: this.#visibleWidth,
|
|
327
391
|
visible: this.#visible,
|
|
328
392
|
wrap: this.#wrap,
|
|
@@ -501,8 +565,17 @@ export class ElementsTreeOutline extends
|
|
|
501
565
|
#issuesManager?: IssuesManager.IssuesManager.IssuesManager;
|
|
502
566
|
#popupHelper?: UI.PopoverHelper.PopoverHelper;
|
|
503
567
|
#nodeElementToIssues = new Map<Element, IssuesManager.Issue.Issue[]>();
|
|
504
|
-
|
|
505
|
-
|
|
568
|
+
readonly maxTreeDepth?: number;
|
|
569
|
+
readonly enableContextMenu: boolean;
|
|
570
|
+
readonly showComments: boolean;
|
|
571
|
+
readonly showAIButton: boolean;
|
|
572
|
+
readonly disableEdits: boolean;
|
|
573
|
+
readonly expandRoot: boolean;
|
|
574
|
+
|
|
575
|
+
constructor(
|
|
576
|
+
omitRootDOMNode?: boolean, selectEnabled?: boolean, hideGutter?: boolean, maxTreeDepth?: number,
|
|
577
|
+
enableContextMenu?: boolean, showComments?: boolean, showAIButton?: boolean, disableEdits?: boolean,
|
|
578
|
+
expandRoot?: boolean) {
|
|
506
579
|
super();
|
|
507
580
|
|
|
508
581
|
this.#issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
|
|
@@ -516,26 +589,35 @@ export class ElementsTreeOutline extends
|
|
|
516
589
|
|
|
517
590
|
this.elementInternal = this.element;
|
|
518
591
|
this.elementInternal.classList.add('elements-tree-outline', 'source-code');
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
592
|
+
this.maxTreeDepth = maxTreeDepth;
|
|
593
|
+
this.enableContextMenu = enableContextMenu ?? true;
|
|
594
|
+
this.showComments = showComments ?? true;
|
|
595
|
+
this.showAIButton = showAIButton ?? true;
|
|
596
|
+
this.disableEdits = disableEdits ?? false;
|
|
597
|
+
this.expandRoot = expandRoot ?? false;
|
|
598
|
+
this.elementInternal.classList.toggle('elements-hide-gutter', hideGutter);
|
|
522
599
|
UI.ARIAUtils.setLabel(this.elementInternal, i18nString(UIStrings.pageDom));
|
|
523
600
|
this.elementInternal.addEventListener('focusout', this.onfocusout.bind(this), false);
|
|
524
601
|
this.elementInternal.addEventListener('mousedown', this.onmousedown.bind(this), false);
|
|
525
602
|
this.elementInternal.addEventListener('mousemove', this.onmousemove.bind(this), false);
|
|
526
603
|
this.elementInternal.addEventListener('mouseleave', this.onmouseleave.bind(this), false);
|
|
527
|
-
this.elementInternal.addEventListener('dragstart', this.ondragstart.bind(this), false);
|
|
528
|
-
this.elementInternal.addEventListener('dragover', this.ondragover.bind(this), false);
|
|
529
|
-
this.elementInternal.addEventListener('dragleave', this.ondragleave.bind(this), false);
|
|
530
|
-
this.elementInternal.addEventListener('drop', this.ondrop.bind(this), false);
|
|
531
|
-
this.elementInternal.addEventListener('dragend', this.ondragend.bind(this), false);
|
|
532
|
-
this.elementInternal.addEventListener('contextmenu', this.contextMenuEventFired.bind(this), false);
|
|
533
|
-
this.elementInternal.addEventListener('clipboard-beforecopy', this.onBeforeCopy.bind(this), false);
|
|
534
|
-
this.elementInternal.addEventListener('clipboard-copy', this.onCopyOrCut.bind(this, false), false);
|
|
535
|
-
this.elementInternal.addEventListener('clipboard-cut', this.onCopyOrCut.bind(this, true), false);
|
|
536
|
-
this.elementInternal.addEventListener('clipboard-paste', this.onPaste.bind(this), false);
|
|
537
604
|
this.elementInternal.addEventListener('keydown', this.onKeyDown.bind(this), false);
|
|
538
605
|
|
|
606
|
+
if (!this.disableEdits) {
|
|
607
|
+
this.elementInternal.addEventListener('dragstart', this.ondragstart.bind(this), false);
|
|
608
|
+
this.elementInternal.addEventListener('dragover', this.ondragover.bind(this), false);
|
|
609
|
+
this.elementInternal.addEventListener('dragleave', this.ondragleave.bind(this), false);
|
|
610
|
+
this.elementInternal.addEventListener('drop', this.ondrop.bind(this), false);
|
|
611
|
+
this.elementInternal.addEventListener('dragend', this.ondragend.bind(this), false);
|
|
612
|
+
this.elementInternal.addEventListener('clipboard-beforecopy', this.onBeforeCopy.bind(this), false);
|
|
613
|
+
this.elementInternal.addEventListener('clipboard-copy', this.onCopyOrCut.bind(this, false), false);
|
|
614
|
+
this.elementInternal.addEventListener('clipboard-cut', this.onCopyOrCut.bind(this, true), false);
|
|
615
|
+
this.elementInternal.addEventListener('clipboard-paste', this.onPaste.bind(this), false);
|
|
616
|
+
}
|
|
617
|
+
if (this.enableContextMenu) {
|
|
618
|
+
this.elementInternal.addEventListener('contextmenu', this.contextMenuEventFired.bind(this), false);
|
|
619
|
+
}
|
|
620
|
+
|
|
539
621
|
outlineDisclosureElement.appendChild(this.elementInternal);
|
|
540
622
|
this.element = shadowContainer;
|
|
541
623
|
this.contentElement.setAttribute('jslog', `${VisualLogging.tree('elements')}`);
|
|
@@ -571,9 +653,16 @@ export class ElementsTreeOutline extends
|
|
|
571
653
|
this.treeElementsBeingUpdated = new Set();
|
|
572
654
|
|
|
573
655
|
this.decoratorExtensions = null;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
656
|
+
if (this.showComments) {
|
|
657
|
+
this.showHTMLCommentsSetting = Common.Settings.Settings.instance().moduleSetting('show-html-comments');
|
|
658
|
+
this.showHTMLCommentsSetting.addChangeListener(this.onShowHTMLCommentsChange.bind(this));
|
|
659
|
+
} else {
|
|
660
|
+
this.showHTMLCommentsSetting = {
|
|
661
|
+
get: () => false,
|
|
662
|
+
addChangeListener: () => {},
|
|
663
|
+
removeChangeListener: () => {},
|
|
664
|
+
} as unknown as Common.Settings.Setting<boolean>;
|
|
665
|
+
}
|
|
577
666
|
this.setUseLightSelectionColor(true);
|
|
578
667
|
// TODO(changhaohan): refactor the popover to use tooltip component.
|
|
579
668
|
this.#popupHelper = new UI.PopoverHelper.PopoverHelper(this.elementInternal, event => {
|
|
@@ -595,19 +684,20 @@ export class ElementsTreeOutline extends
|
|
|
595
684
|
render(html`
|
|
596
685
|
<div class="squiggles-content">
|
|
597
686
|
${issues.map(issue => {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
687
|
+
const elementIssueDetails = getElementIssueDetails(issue);
|
|
688
|
+
if (!elementIssueDetails) {
|
|
689
|
+
// This shouldn't happen, but add this if check to pass ts check.
|
|
690
|
+
return nothing;
|
|
691
|
+
}
|
|
692
|
+
const issueKindIconName = IssueCounter.IssueCounter.getIssueKindIconName(issue.getKind());
|
|
693
|
+
const openIssueEvent = (): Promise<void> => Common.Revealer.reveal(issue);
|
|
694
|
+
return html`
|
|
606
695
|
<div class="squiggles-content-item">
|
|
607
696
|
<devtools-icon .name=${issueKindIconName} @click=${openIssueEvent}></devtools-icon>
|
|
608
697
|
<devtools-link class="link" @click=${openIssueEvent}>${i18nString(UIStrings.viewIssue)}</devtools-link>
|
|
609
698
|
<span>${elementIssueDetails.tooltip}</span>
|
|
610
|
-
</div>`;
|
|
699
|
+
</div>`;
|
|
700
|
+
})}
|
|
611
701
|
</div>`, popover.contentElement);
|
|
612
702
|
// clang-format on
|
|
613
703
|
return true;
|
|
@@ -958,6 +1048,9 @@ export class ElementsTreeOutline extends
|
|
|
958
1048
|
if (this.includeRootDOMNode) {
|
|
959
1049
|
const treeElement = this.createElementTreeElement(this.rootDOMNode);
|
|
960
1050
|
this.appendChild(treeElement);
|
|
1051
|
+
if (this.expandRoot) {
|
|
1052
|
+
treeElement.expand();
|
|
1053
|
+
}
|
|
961
1054
|
} else {
|
|
962
1055
|
// FIXME: this could use findTreeElement to reuse a tree element if it already exists
|
|
963
1056
|
const children = this.visibleChildren(this.rootDOMNode);
|
|
@@ -1774,6 +1867,31 @@ export class ElementsTreeOutline extends
|
|
|
1774
1867
|
}
|
|
1775
1868
|
}
|
|
1776
1869
|
|
|
1870
|
+
private isMaxDepthReached(node: SDK.DOMModel.DOMNode): boolean {
|
|
1871
|
+
if (this.maxTreeDepth === undefined || this.maxTreeDepth === Infinity) {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
// Allow ShadowRoots and Documents to expand one more level.
|
|
1875
|
+
if (node.nodeType() === Node.DOCUMENT_NODE || node.isShadowRoot()) {
|
|
1876
|
+
return false;
|
|
1877
|
+
}
|
|
1878
|
+
const maxDepth = this.maxTreeDepth;
|
|
1879
|
+
let depth = 0;
|
|
1880
|
+
let current: SDK.DOMModel.DOMNode|null = node;
|
|
1881
|
+
const rootNode = this.rootDOMNode;
|
|
1882
|
+
while (current && current !== rootNode) {
|
|
1883
|
+
depth++;
|
|
1884
|
+
current = current.parentNode;
|
|
1885
|
+
}
|
|
1886
|
+
if (this.includeRootDOMNode) {
|
|
1887
|
+
depth++;
|
|
1888
|
+
}
|
|
1889
|
+
if (depth >= maxDepth) {
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1777
1895
|
private createElementTreeElement(node: SDK.DOMModel.DOMNode|SDK.DOMModel.AdoptedStyleSheet[], isClosingTag?: boolean):
|
|
1778
1896
|
UI.TreeOutline.TreeElement {
|
|
1779
1897
|
if (node instanceof Array) {
|
|
@@ -1870,6 +1988,9 @@ export class ElementsTreeOutline extends
|
|
|
1870
1988
|
}
|
|
1871
1989
|
|
|
1872
1990
|
private hasVisibleChildren(node: SDK.DOMModel.DOMNode): boolean {
|
|
1991
|
+
if (this.isMaxDepthReached(node)) {
|
|
1992
|
+
return false;
|
|
1993
|
+
}
|
|
1873
1994
|
if (node.isIframe()) {
|
|
1874
1995
|
return true;
|
|
1875
1996
|
}
|
|
@@ -63,6 +63,11 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
63
63
|
let lighthousePanelInstace: LighthousePanel;
|
|
64
64
|
type Nullable<T> = T|null;
|
|
65
65
|
|
|
66
|
+
export class ActiveLighthouseReport {
|
|
67
|
+
constructor(public report: LighthouseModel.ReporterTypes.ReportJSON) {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
export class LighthousePanel extends UI.Panel.Panel {
|
|
67
72
|
private readonly controller: LighthouseController;
|
|
68
73
|
private readonly startView: StartView;
|
|
@@ -108,6 +113,7 @@ export class LighthousePanel extends UI.Panel.Panel {
|
|
|
108
113
|
this.renderStartView();
|
|
109
114
|
|
|
110
115
|
this.controller.recomputePageAuditability();
|
|
116
|
+
UI.Context.Context.instance().setFlavor(ActiveLighthouseReport, null);
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
static instance(opts?: {forceNew: boolean, protocolService: ProtocolService, controller: LighthouseController}):
|
|
@@ -154,9 +160,11 @@ export class LighthousePanel extends UI.Panel.Panel {
|
|
|
154
160
|
this.renderStatusView();
|
|
155
161
|
const {lhr, artifacts} = await this.controller.collectLighthouseResults();
|
|
156
162
|
this.buildReportUI(lhr, artifacts);
|
|
163
|
+
UI.Context.Context.instance().setFlavor(ActiveLighthouseReport, new ActiveLighthouseReport(lhr));
|
|
157
164
|
return {report: lhr};
|
|
158
165
|
} catch (err) {
|
|
159
166
|
this.handleError(err);
|
|
167
|
+
UI.Context.Context.instance().setFlavor(ActiveLighthouseReport, null);
|
|
160
168
|
return {report: null};
|
|
161
169
|
}
|
|
162
170
|
}
|
|
@@ -636,7 +636,7 @@ const GREENDEV_VIEW: View = (input, _output, target) => {
|
|
|
636
636
|
<span>${i18nString(UIStrings.greenDevUnstable)}</span>
|
|
637
637
|
</div>
|
|
638
638
|
<div class="settings-experiments-block">
|
|
639
|
-
${renderPrototypeCheckboxes(input.settings, ['aiAnnotations', 'copyToGemini', 'breakpointDebuggerAgent'])}
|
|
639
|
+
${renderPrototypeCheckboxes(input.settings, ['aiAnnotations', 'copyToGemini', 'breakpointDebuggerAgent', 'emulationCapabilities'])}
|
|
640
640
|
</div>
|
|
641
641
|
</devtools-card>
|
|
642
642
|
</div>
|
|
@@ -647,7 +647,8 @@ const GREENDEV_VIEW: View = (input, _output, target) => {
|
|
|
647
647
|
const GREENDEV_PROTOTYPE_NAMES: Record<keyof GreenDev.GreenDevSettings, string> = {
|
|
648
648
|
aiAnnotations: 'AI auto-annotations',
|
|
649
649
|
copyToGemini: 'Copy changes to AI Prompt',
|
|
650
|
-
breakpointDebuggerAgent: 'Breakpoint Debugger Agent'
|
|
650
|
+
breakpointDebuggerAgent: 'Breakpoint Debugger Agent',
|
|
651
|
+
emulationCapabilities: 'Emulation Capabilities',
|
|
651
652
|
};
|
|
652
653
|
|
|
653
654
|
function renderPrototypeCheckboxes(
|
|
@@ -2094,10 +2094,7 @@ export class HTMLElementWithLightDOMTemplate extends HTMLElement {
|
|
|
2094
2094
|
const patchingWrapper = <Args extends any[], R>(fn: (...args: Args) => R): ((...args: Args) => R) => {
|
|
2095
2095
|
return function(this: unknown, ...args: Args): R {
|
|
2096
2096
|
const result = fn.apply(this, args);
|
|
2097
|
-
|
|
2098
|
-
HTMLElementWithLightDOMTemplate.patchLitTemplate(result);
|
|
2099
|
-
}
|
|
2100
|
-
return result;
|
|
2097
|
+
return patchValue(result) as R;
|
|
2101
2098
|
};
|
|
2102
2099
|
};
|
|
2103
2100
|
if (template === Lit.nothing) {
|
|
@@ -2117,7 +2114,10 @@ export class HTMLElementWithLightDOMTemplate extends HTMLElement {
|
|
|
2117
2114
|
|
|
2118
2115
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2119
2116
|
function isCallable(value: unknown): value is(...args: any[]) => any {
|
|
2120
|
-
|
|
2117
|
+
// Native class constructors cannot be invoked without 'new', and we shouldn't attempt to wrap them.
|
|
2118
|
+
// Differentiate them from regular functions by checking their 'prototype' descriptor:
|
|
2119
|
+
// class constructors have a non-writable prototype, whereas regular functions have a writable prototype.
|
|
2120
|
+
return typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype')?.writable !== false;
|
|
2121
2121
|
}
|
|
2122
2122
|
|
|
2123
2123
|
function patchValue(value: unknown): unknown {
|
|
@@ -38,6 +38,8 @@ import * as Lit from '../../ui/lit/lit.js';
|
|
|
38
38
|
import {appendStyle, deepActiveElement} from './DOMUtilities.js';
|
|
39
39
|
import {cloneCustomElement, createShadowRootWithCoreStyles} from './UIUtils.js';
|
|
40
40
|
|
|
41
|
+
const {html} = Lit;
|
|
42
|
+
|
|
41
43
|
// Remember the original DOM mutation methods here, since we
|
|
42
44
|
// will override them below to sanity check the Widget system.
|
|
43
45
|
const originalAppendChild = Element.prototype.appendChild;
|
|
@@ -223,7 +225,7 @@ export class WidgetElement<WidgetT extends Widget> extends HTMLElement {
|
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
override removeChild<T extends Node>(child: T): T {
|
|
226
|
-
const childWidget = Widget.get(child
|
|
228
|
+
const childWidget = Widget.get(child);
|
|
227
229
|
if (childWidget) {
|
|
228
230
|
childWidget.detach();
|
|
229
231
|
return child;
|
|
@@ -233,7 +235,7 @@ export class WidgetElement<WidgetT extends Widget> extends HTMLElement {
|
|
|
233
235
|
|
|
234
236
|
override removeChildren(): void {
|
|
235
237
|
for (const child of this.children) {
|
|
236
|
-
const childWidget = Widget.get(child
|
|
238
|
+
const childWidget = Widget.get(child);
|
|
237
239
|
if (childWidget) {
|
|
238
240
|
childWidget.detach();
|
|
239
241
|
}
|
|
@@ -263,6 +265,35 @@ export class WidgetElement<WidgetT extends Widget> extends HTMLElement {
|
|
|
263
265
|
|
|
264
266
|
customElements.define('devtools-widget', WidgetElement);
|
|
265
267
|
|
|
268
|
+
export class WidgetDirective extends Lit.Directive.Directive {
|
|
269
|
+
constructor(partInfo: Lit.Directive.PartInfo) {
|
|
270
|
+
super(partInfo);
|
|
271
|
+
if (partInfo.type !== Lit.Directive.PartType.CHILD) {
|
|
272
|
+
throw new Error('Widget directive must be used as a child directive.');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
render<F extends WidgetFactory<Widget>, ParamKeys extends keyof InferWidgetTFromFactory<F>>(
|
|
277
|
+
widgetClass: F,
|
|
278
|
+
widgetParams?: Pick<InferWidgetTFromFactory<F>, ParamKeys>&Partial<InferWidgetTFromFactory<F>>): unknown {
|
|
279
|
+
// We use `repeat` to force Lit to recreate the `<devtools-widget>` DOM node when the `widgetClass` changes.
|
|
280
|
+
// If we didn't use `repeat` and used `html` directly, Lit would reuse the same `<devtools-widget>` instance
|
|
281
|
+
// even if `widgetClass` changed (for example, in a ternary operator `condition ? widget(A) : widget(B)`).
|
|
282
|
+
// This is because the template string is the same, so Lit reuses the DOM node and only updates `.widgetConfig`,
|
|
283
|
+
// which does not properly recreate the widget instance.
|
|
284
|
+
return Lit.Directives.repeat(
|
|
285
|
+
[widgetClass], () => widgetClass,
|
|
286
|
+
() => html`<devtools-widget .widgetConfig=${
|
|
287
|
+
widgetConfig(widgetClass, widgetParams as never)}></devtools-widget>`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const widget = Lit.Directive.directive(WidgetDirective) as
|
|
292
|
+
<F extends WidgetFactory<Widget>, ParamKeys extends keyof InferWidgetTFromFactory<F>>(
|
|
293
|
+
widgetClass: F,
|
|
294
|
+
widgetParams?: Pick<InferWidgetTFromFactory<F>, ParamKeys>&
|
|
295
|
+
Partial<InferWidgetTFromFactory<F>>) => Lit.Directive.DirectiveResult<typeof WidgetDirective>;
|
|
296
|
+
|
|
266
297
|
export function widgetRef<T extends Widget, Args extends unknown[]>(
|
|
267
298
|
type: Platform.Constructor.Constructor<T, Args>, callback: (_: T) => void): ReturnType<typeof Lit.Directives.ref> {
|
|
268
299
|
return Lit.Directives.ref((e?: Element) => {
|
|
@@ -1808,6 +1808,7 @@ export const knownContextValues = new Set([
|
|
|
1808
1808
|
'greendev-artifact-viewer-enabled',
|
|
1809
1809
|
'greendev-breakpoint-debugger-agent-enabled',
|
|
1810
1810
|
'greendev-copy-to-gemini-enabled',
|
|
1811
|
+
'greendev-emulation-capabilities-enabled',
|
|
1811
1812
|
'greendev-in-devtools-floaty-enabled',
|
|
1812
1813
|
'greendev-inline-widgets-enabled',
|
|
1813
1814
|
'greendev-prototypes',
|
package/package.json
CHANGED