| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import * as Common from '../../core/common/common.js'; | |
| import * as i18n from '../../core/i18n/i18n.js'; | |
| import * as SDK from '../../core/sdk/sdk.js'; | |
| import * as UI from '../../ui/legacy/legacy.js'; | |
| import * as Emulation from '../emulation/emulation.js'; | |
| import {canGetJSPath} from './DOMPath.js'; | |
| import {DOMTreeElementWidget} from './DOMTreeElementWidget.js'; | |
| import type {DOMTreeWidget} from './DOMTreeWidget.js'; | |
| import {ElementsPanel} from './ElementsPanel.js'; | |
| const UIStrings = { | |
| /** | |
| * @description A context menu item to store a value as a global variable the Elements Panel | |
| */ | |
| storeAsGlobalVariable: 'Store as global variable', | |
| /** | |
| * @description A context menu item in the Elements Tree Element of the Elements panel | |
| */ | |
| addAttribute: 'Add attribute', | |
| /** | |
| * @description Text to modify the attribute of an item | |
| */ | |
| editAttribute: 'Edit attribute', | |
| /** | |
| * @description Text to focus on something | |
| */ | |
| focus: 'Focus', | |
| /** | |
| * @description Text to scroll the displayed content into view | |
| */ | |
| scrollIntoView: 'Scroll into view', | |
| /** | |
| * @description A context menu item in the Elements panel to switch to Accessibility tree | |
| */ | |
| switchToAccessibilityTree: 'Switch to accessibility tree', | |
| /** | |
| * @description A context menu item in the Elements Tree Element of the Elements panel | |
| */ | |
| editText: 'Edit text', | |
| /** | |
| * @description A context menu item in the Elements Tree Element of the Elements panel | |
| */ | |
| editAsHtml: 'Edit as HTML', | |
| /** | |
| * @description A context menu item in the Elements Tree Element of the Elements panel | |
| */ | |
| editData: 'Edit data', | |
| /** | |
| * @description Text to cut an element, cut should be used as a verb | |
| */ | |
| cut: 'Cut', | |
| /** | |
| * @description Text for copying, copy should be used as a verb | |
| */ | |
| copy: 'Copy', | |
| /** | |
| * @description Text to paste an element, paste should be used as a verb | |
| */ | |
| paste: 'Paste', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb | |
| */ | |
| copyOuterhtml: 'Copy outerHTML', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb | |
| */ | |
| copySelector: 'Copy `selector`', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel | |
| */ | |
| copyJsPath: 'Copy JS path', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb | |
| */ | |
| copyStyles: 'Copy styles', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb | |
| */ | |
| copyXpath: 'Copy XPath', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb | |
| */ | |
| copyFullXpath: 'Copy full XPath', | |
| /** | |
| * @description Text in Elements Tree Element of the Elements panel, copy should be used as a verb | |
| */ | |
| copyElement: 'Copy element', | |
| /** | |
| * @description A context menu item in the Elements Tree Element of the Elements panel | |
| */ | |
| duplicateElement: 'Duplicate element', | |
| /** | |
| * @description Text to hide an element | |
| */ | |
| hideElement: 'Hide element', | |
| /** | |
| * @description A context menu item in the Elements Tree Element of the Elements panel | |
| */ | |
| deleteElement: 'Delete element', | |
| /** | |
| * @description Text to expand something recursively | |
| */ | |
| expandRecursively: 'Expand recursively', | |
| /** | |
| * @description Text to collapse children of a parent group | |
| */ | |
| collapseChildren: 'Collapse children', | |
| /** | |
| * @description Title of an action in the emulation tool to capture node screenshot | |
| */ | |
| captureNodeScreenshot: 'Capture node screenshot', | |
| /** | |
| * @description Title of a context menu item. When clicked DevTools goes to the Application panel and shows this specific iframe's details | |
| */ | |
| showFrameDetails: 'Show `iframe` details', | |
| /** | |
| * @description Text of a context menu item to redirect to the AI assistance panel and to start a chat. | |
| */ | |
| startAChat: 'Start a chat', | |
| /** | |
| * @description Context menu item in Elements panel to assess visibility of an element via AI. | |
| */ | |
| assessVisibility: 'Assess visibility', | |
| /** | |
| * @description Context menu item in Elements panel to center an element via AI. | |
| */ | |
| centerElement: 'Center element', | |
| /** | |
| * @description Context menu item in Elements panel to wrap flex items via AI. | |
| */ | |
| wrapTheseItems: 'Wrap these items', | |
| /** | |
| * @description Context menu item in Elements panel to distribute flex items evenly via AI. | |
| */ | |
| distributeItemsEvenly: 'Distribute items evenly', | |
| /** | |
| * @description Context menu item in Elements panel to explain flexbox via AI. | |
| */ | |
| explainFlexbox: 'Explain flexbox', | |
| /** | |
| * @description Context menu item in Elements panel to align grid items via AI. | |
| */ | |
| alignItems: 'Align items', | |
| /** | |
| * @description Context menu item in Elements panel to add padding/gap to grid via AI. | |
| */ | |
| addPadding: 'Add padding', | |
| /** | |
| * @description Context menu item in Elements panel to explain grid layout via AI. | |
| */ | |
| explainGridLayout: 'Explain grid layout', | |
| /** | |
| * @description Context menu item in Elements panel to find grid definition for a subgrid item via AI. | |
| */ | |
| findGridDefinition: 'Find grid definition', | |
| /** | |
| * @description Context menu item in Elements panel to change parent grid properties for a subgrid item via AI. | |
| */ | |
| changeParentProperties: 'Change parent properties', | |
| /** | |
| * @description Context menu item in Elements panel to explain subgrids via AI. | |
| */ | |
| explainSubgrids: 'Explain subgrids', | |
| /** | |
| * @description Context menu item in Elements panel to remove scrollbars via AI. | |
| */ | |
| removeScrollbars: 'Remove scrollbars', | |
| /** | |
| * @description Context menu item in Elements panel to style scrollbars via AI. | |
| */ | |
| styleScrollbars: 'Style scrollbars', | |
| /** | |
| * @description Context menu item in Elements panel to explain scrollbars via AI. | |
| */ | |
| explainScrollbars: 'Explain scrollbars', | |
| /** | |
| * @description Context menu item in Elements panel to explain container queries via AI. | |
| */ | |
| explainContainerQueries: 'Explain container queries', | |
| /** | |
| * @description Context menu item in Elements panel to explain container types via AI. | |
| */ | |
| explainContainerTypes: 'Explain container types', | |
| /** | |
| * @description Context menu item in Elements panel to explain container context via AI. | |
| */ | |
| explainContainerContext: 'Explain container context', | |
| } as const; | |
| const str_ = i18n.i18n.registerUIStrings('panels/elements/DOMTreeContextMenu.ts', UIStrings); | |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); | |
| export async function populateNodeContextMenu( | |
| contextMenu: UI.ContextMenu.ContextMenu, | |
| domTreeWidget: DOMTreeWidget, | |
| domNode: SDK.DOMModel.DOMNode, | |
| ): Promise<void> { | |
| const isEditable = !domNode.isShadowRoot() && !domNode.ancestorUserAgentShadowRoot(); | |
| if (isEditable) { | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.editAsHtml), | |
| () => domTreeWidget.toggleEditAsHTML(domNode), | |
| {jslogContext: 'elements.edit-as-html'}); | |
| } | |
| const isShadowRoot = domNode.isShadowRoot(); | |
| const createShortcut = UI.KeyboardShortcut.KeyboardShortcut.shortcutToString.bind(null); | |
| const modifier = UI.KeyboardShortcut.Modifiers.CtrlOrMeta.value; | |
| let menuItem; | |
| const openAiAssistanceId = 'freestyler.element-panel-context'; | |
| if (UI.ActionRegistry.ActionRegistry.instance().hasAction(openAiAssistanceId)) { | |
| function appendSubmenuPromptAction(submenu: UI.ContextMenu.SubMenu, action: UI.ActionRegistration.Action, | |
| label: Common.UIString.LocalizedString, prompt: string, | |
| jslogContext: string): void { | |
| submenu.defaultSection().appendItem(label, () => { | |
| void action.execute({prompt}); | |
| UI.UIUtils.PromotionManager.instance().recordFeatureInteraction(openAiAssistanceId); | |
| }, {disabled: !action.enabled(), jslogContext}); | |
| } | |
| UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, domNode); | |
| const action = UI.ActionRegistry.ActionRegistry.instance().getAction(openAiAssistanceId); | |
| const submenu = contextMenu.footerSection().appendSubMenuItem(action.title(), false, openAiAssistanceId); | |
| submenu.defaultSection().appendAction(openAiAssistanceId, i18nString(UIStrings.startAChat)); | |
| const submenuConfigs = [ | |
| { | |
| condition: (props: SDK.CSSModel.LayoutProperties|null): boolean => Boolean(props?.isFlex), | |
| items: [ | |
| { | |
| label: i18nString(UIStrings.wrapTheseItems), | |
| prompt: 'How can I make flex items wrap?', | |
| jslogContextSuffix: '.flex-wrap', | |
| }, | |
| { | |
| label: i18nString(UIStrings.distributeItemsEvenly), | |
| prompt: 'How do I distribute flex items evenly?', | |
| jslogContextSuffix: '.flex-distribute', | |
| }, | |
| { | |
| label: i18nString(UIStrings.explainFlexbox), | |
| prompt: 'What is flexbox?', | |
| jslogContextSuffix: '.flex-what', | |
| }, | |
| ], | |
| }, | |
| { | |
| condition: (props: SDK.CSSModel.LayoutProperties|null): boolean => Boolean(props?.isGrid && !props?.isSubgrid), | |
| items: [ | |
| { | |
| label: i18nString(UIStrings.alignItems), | |
| prompt: 'How do I align items in a grid?', | |
| jslogContextSuffix: '.grid-align', | |
| }, | |
| { | |
| label: i18nString(UIStrings.addPadding), | |
| prompt: 'How to add spacing between grid items?', | |
| jslogContextSuffix: '.grid-gap', | |
| }, | |
| { | |
| label: i18nString(UIStrings.explainGridLayout), | |
| prompt: 'How does grid layout work?', | |
| jslogContextSuffix: '.grid-how', | |
| }, | |
| ], | |
| }, | |
| { | |
| condition: (props: SDK.CSSModel.LayoutProperties|null): boolean => Boolean(props?.isSubgrid), | |
| items: [ | |
| { | |
| label: i18nString(UIStrings.findGridDefinition), | |
| prompt: 'Where is this grid defined?', | |
| jslogContextSuffix: '.subgrid-where', | |
| }, | |
| { | |
| label: i18nString(UIStrings.changeParentProperties), | |
| prompt: 'How to overwrite parent grid properties?', | |
| jslogContextSuffix: '.subgrid-override', | |
| }, | |
| { | |
| label: i18nString(UIStrings.explainSubgrids), | |
| prompt: 'How do subgrids work?', | |
| jslogContextSuffix: '.subgrid-how', | |
| }, | |
| ], | |
| }, | |
| { | |
| condition: (props: SDK.CSSModel.LayoutProperties|null): boolean => Boolean(props?.hasScroll), | |
| items: [ | |
| { | |
| label: i18nString(UIStrings.removeScrollbars), | |
| prompt: 'How do I remove scrollbars for this element?', | |
| jslogContextSuffix: '.scroll-remove', | |
| }, | |
| { | |
| label: i18nString(UIStrings.styleScrollbars), | |
| prompt: 'How can I style a scrollbar?', | |
| jslogContextSuffix: '.scroll-style', | |
| }, | |
| { | |
| label: i18nString(UIStrings.explainScrollbars), | |
| prompt: 'Why does this element scroll?', | |
| jslogContextSuffix: '.scroll-why', | |
| }, | |
| ], | |
| }, | |
| { | |
| condition: (props: SDK.CSSModel.LayoutProperties|null): boolean => Boolean(props?.containerType), | |
| items: [ | |
| { | |
| label: i18nString(UIStrings.explainContainerQueries), | |
| prompt: 'What are container queries?', | |
| jslogContextSuffix: '.container-what', | |
| }, | |
| { | |
| label: i18nString(UIStrings.explainContainerTypes), | |
| prompt: 'How do I use container-type?', | |
| jslogContextSuffix: '.container-how', | |
| }, | |
| { | |
| label: i18nString(UIStrings.explainContainerContext), | |
| prompt: 'What\'s the container context for this element?', | |
| jslogContextSuffix: '.container-context', | |
| }, | |
| ], | |
| }, | |
| { | |
| // Default items | |
| condition: (): boolean => true, | |
| items: [ | |
| { | |
| label: i18nString(UIStrings.assessVisibility), | |
| prompt: 'Why isn’t this element visible?', | |
| jslogContextSuffix: '.visibility', | |
| }, | |
| { | |
| label: i18nString(UIStrings.centerElement), | |
| prompt: 'How do I center this element?', | |
| jslogContextSuffix: '.center', | |
| }, | |
| ], | |
| }, | |
| ]; | |
| const layoutProps = await domNode.domModel().cssModel().getLayoutPropertiesFromComputedStyle(domNode.id); | |
| const config = submenuConfigs.find(c => c.condition(layoutProps)); | |
| if (config) { | |
| for (const item of config.items) { | |
| appendSubmenuPromptAction(submenu, action, item.label, item.prompt, | |
| openAiAssistanceId + item.jslogContextSuffix); | |
| } | |
| } | |
| } | |
| menuItem = contextMenu.clipboardSection().appendItem(i18nString(UIStrings.cut), | |
| () => domTreeWidget.performCopyOrCut(true, domNode), | |
| {disabled: !isEditable, jslogContext: 'cut'}); | |
| menuItem.setShortcut(createShortcut('X', modifier)); | |
| // Place it here so that all "Copy"-ing items stick together. | |
| const copyMenu = contextMenu.clipboardSection().appendSubMenuItem(i18nString(UIStrings.copy), false, 'copy'); | |
| const section = copyMenu.section(); | |
| if (!isShadowRoot) { | |
| menuItem = | |
| section.appendItem(i18nString(UIStrings.copyOuterhtml), () => domTreeWidget.performCopyOrCut(false, domNode), | |
| {jslogContext: 'copy-outer-html'}); | |
| menuItem.setShortcut(createShortcut('V', modifier)); | |
| } | |
| if (domNode.nodeType() === Node.ELEMENT_NODE) { | |
| section.appendItem(i18nString(UIStrings.copySelector), () => domTreeWidget.copyCSSPath(domNode), | |
| {jslogContext: 'copy-selector'}); | |
| section.appendItem(i18nString(UIStrings.copyJsPath), () => domTreeWidget.copyJSPath(domNode), | |
| {disabled: !canGetJSPath(domNode), jslogContext: 'copy-js-path'}); | |
| section.appendItem(i18nString(UIStrings.copyStyles), () => void domTreeWidget.copyStyles(domNode), | |
| {jslogContext: 'elements.copy-styles'}); | |
| } | |
| if (!isShadowRoot) { | |
| section.appendItem(i18nString(UIStrings.copyXpath), () => void domTreeWidget.copyXPath(domNode), | |
| {jslogContext: 'copy-xpath'}); | |
| section.appendItem(i18nString(UIStrings.copyFullXpath), () => void domTreeWidget.copyFullXPath(domNode), | |
| {jslogContext: 'copy-full-xpath'}); | |
| } | |
| menuItem = copyMenu.clipboardSection().appendItem(i18nString(UIStrings.copyElement), | |
| () => domTreeWidget.performCopyOrCut(false, domNode, true), | |
| {jslogContext: 'copy-element'}); | |
| menuItem.setShortcut(createShortcut('C', modifier)); | |
| if (!isShadowRoot) { | |
| // Duplicate element, disabled on root element and ShadowDOM. | |
| const isRootElement = !domNode.parentNode || domNode.parentNode.nodeName() === '#document'; | |
| menuItem = contextMenu.editSection().appendItem(i18nString(UIStrings.duplicateElement), | |
| () => domTreeWidget.duplicateNode(domNode), { | |
| disabled: (domNode.isInShadowTree() || isRootElement), | |
| jslogContext: 'elements.duplicate-element', | |
| }); | |
| } | |
| menuItem = | |
| contextMenu.clipboardSection().appendItem(i18nString(UIStrings.paste), () => domTreeWidget.pasteNode(domNode), | |
| {disabled: !domTreeWidget.canPaste(domNode), jslogContext: 'paste'}); | |
| menuItem.setShortcut(createShortcut('V', modifier)); | |
| menuItem = contextMenu.debugSection().appendCheckboxItem(i18nString(UIStrings.hideElement), | |
| () => domTreeWidget.toggleHideElement(domNode), { | |
| checked: Boolean(domTreeWidget.isToggledToHidden(domNode)), | |
| jslogContext: 'elements.hide-element', | |
| }); | |
| menuItem.setShortcut( | |
| UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutTitleForAction('elements.hide-element') || ''); | |
| if (isEditable) { | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.deleteElement), | |
| () => void domTreeWidget.removeNode(domNode), | |
| {jslogContext: 'delete-element'}); | |
| } | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.expandRecursively), | |
| () => void domTreeWidget.expandRecursively(domNode), | |
| {jslogContext: 'expand-recursively'}); | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.collapseChildren), | |
| () => domTreeWidget.collapseChildren(domNode), | |
| {jslogContext: 'collapse-children'}); | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.switchToAccessibilityTree), | |
| () => ElementsPanel.instance().toggleAccessibilityTree(), | |
| {jslogContext: 'switch-to-accessibility-tree'}); | |
| const deviceModeWrapperAction = new Emulation.DeviceModeView.ActionDelegate(); | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.captureNodeScreenshot), | |
| deviceModeWrapperAction.handleAction.bind(null, UI.Context.Context.instance(), | |
| 'emulation.capture-node-screenshot'), | |
| {jslogContext: 'emulation.capture-node-screenshot'}); | |
| if (domNode.frameOwnerFrameId()) { | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.showFrameDetails), () => { | |
| const frameOwnerFrameId = domNode.frameOwnerFrameId(); | |
| if (frameOwnerFrameId) { | |
| const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameOwnerFrameId); | |
| void Common.Revealer.reveal(frame); | |
| } | |
| }, {jslogContext: 'show-frame-details'}); | |
| } | |
| } | |
| export async function showContextMenu( | |
| domTreeWidget: DOMTreeWidget, | |
| domNode: SDK.DOMModel.DOMNode, | |
| event: Event, | |
| ): Promise<UI.ContextMenu.ContextMenu|undefined> { | |
| if (UI.UIUtils.isEditing()) { | |
| return; | |
| } | |
| if (!domTreeWidget.enableContextMenu) { | |
| return; | |
| } | |
| const node = (event.target as Node | null); | |
| if (!node) { | |
| return; | |
| } | |
| // The context menu construction may be async. In order to | |
| // make sure that no other (default) context menu shows up, we need | |
| // to stop propagating and prevent the default action. | |
| event.stopPropagation(); | |
| event.preventDefault(); | |
| const contextMenu = new UI.ContextMenu.ContextMenu(event); | |
| const isPseudoElement = Boolean(domNode.pseudoType()); | |
| const isTag = domNode.nodeType() === Node.ELEMENT_NODE && !isPseudoElement; | |
| let textNode: Element|null = (node as Element).enclosingNodeOrSelfWithClass?.('webkit-html-text-node'); | |
| if (textNode?.classList.contains('bogus')) { | |
| textNode = null; | |
| } | |
| const commentNode = (node as Element).enclosingNodeOrSelfWithClass?.('webkit-html-comment'); | |
| contextMenu.saveSection().appendItem(i18nString(UIStrings.storeAsGlobalVariable), | |
| () => void domNode.saveNodeToTempVariable(), | |
| {jslogContext: 'store-as-global-variable'}); | |
| if (textNode) { | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.editText), | |
| () => domTreeWidget.startEditingTextNode(domNode), { | |
| disabled: !DOMTreeElementWidget.canEditTextNode(domNode), | |
| jslogContext: 'edit-text', | |
| }); | |
| await populateNodeContextMenu(contextMenu, domTreeWidget, domNode); | |
| } else if (isTag) { | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.addAttribute), | |
| () => domTreeWidget.addNewAttribute(domNode), {jslogContext: 'add-attribute'}); | |
| const target = (event.composedPath()[0] || event.target) as Element; | |
| const attribute = target.enclosingNodeOrSelfWithClass?.('webkit-html-attribute'); | |
| const newAttribute = target.enclosingNodeOrSelfWithClass?.('add-attribute'); | |
| if (attribute && !newAttribute) { | |
| const attributeName = attribute.querySelector('.webkit-html-attribute-name')?.textContent?.trim(); | |
| if (attributeName) { | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.editAttribute), | |
| () => domTreeWidget.startEditing(domNode, attributeName), | |
| {jslogContext: 'edit-attribute'}); | |
| } | |
| } | |
| await populateNodeContextMenu(contextMenu, domTreeWidget, domNode); | |
| DOMTreeElementWidget.populateForcedPseudoStateItems(contextMenu, domNode); | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.scrollIntoView), () => domNode.scrollIntoView(), | |
| {jslogContext: 'scroll-into-view'}); | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.focus), async () => { | |
| await domNode.focus(); | |
| }, {jslogContext: 'focus'}); | |
| } else if (commentNode) { | |
| await populateNodeContextMenu(contextMenu, domTreeWidget, domNode); | |
| } else if (isPseudoElement) { | |
| const copyMenu = contextMenu.clipboardSection().appendSubMenuItem(i18nString(UIStrings.copy), false, 'copy'); | |
| const section = copyMenu.section(); | |
| section.appendItem(i18nString(UIStrings.copySelector), () => domTreeWidget.copyCSSPath(domNode), | |
| {jslogContext: 'copy-selector'}); | |
| section.appendItem(i18nString(UIStrings.copyStyles), () => void domTreeWidget.copyStyles(domNode), | |
| {jslogContext: 'elements.copy-styles'}); | |
| const hideItem = contextMenu.debugSection().appendCheckboxItem( | |
| i18nString(UIStrings.hideElement), () => domTreeWidget.toggleHideElement(domNode), { | |
| checked: Boolean(domTreeWidget.isToggledToHidden(domNode)), | |
| jslogContext: 'elements.hide-element', | |
| }); | |
| hideItem.setShortcut( | |
| UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutTitleForAction('elements.hide-element') || ''); | |
| if (domNode.childNodeCount() !== 0 || domNode.hasPseudoElements()) { | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.expandRecursively), | |
| () => void domTreeWidget.expandRecursively(domNode), | |
| {jslogContext: 'expand-recursively'}); | |
| } | |
| contextMenu.viewSection().appendItem(i18nString(UIStrings.scrollIntoView), () => domNode.scrollIntoView(), | |
| {jslogContext: 'scroll-into-view'}); | |
| } else if (domNode.nodeType() === Node.PROCESSING_INSTRUCTION_NODE) { | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.editData), () => domTreeWidget.startEditing(domNode), { | |
| disabled: !DOMTreeElementWidget.canEditTextNode(domNode), | |
| jslogContext: 'elements.edit-data', | |
| }); | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.duplicateElement), | |
| () => domTreeWidget.duplicateNode(domNode), { | |
| disabled: domNode.isInShadowTree(), | |
| jslogContext: 'elements.duplicate-element', | |
| }); | |
| contextMenu.editSection().appendItem(i18nString(UIStrings.deleteElement), | |
| () => void domTreeWidget.removeNode(domNode), | |
| {jslogContext: 'delete-element'}); | |
| } | |
| ElementsPanel.instance().populateAdornerSettingsContextMenu(contextMenu); | |
| contextMenu.appendApplicableItems(domNode); | |
| await contextMenu.show(); | |
| return contextMenu; | |
| } |