11// Copyright 2018 The Chromium Authors. All rights reserved.
22// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file.
4- /* eslint-disable rulesdir/no-imperative-dom-api */
54
65import * as Common from '../../core/common/common.js' ;
76import * as i18n from '../../core/i18n/i18n.js' ;
87import * as SDK from '../../core/sdk/sdk.js' ;
98import * as UI from '../../ui/legacy/legacy.js' ;
9+ import { Directives , html , nothing , render } from '../../ui/lit/lit.js' ;
1010import * as VisualLogging from '../../ui/visual_logging/visual_logging.js' ;
1111
1212import domLinkifierStyles from './domLinkifier.css.js' ;
1313
14+ const { classMap} = Directives ;
15+
1416const UIStrings = {
1517 /**
1618 * @description Text displayed when trying to create a link to a node in the UI, but the node
@@ -27,163 +29,196 @@ export interface Options extends Common.Linkifier.Options {
2729 disabled ?: boolean ;
2830}
2931
30- export const decorateNodeLabel = function (
31- node : SDK . DOMModel . DOMNode , parentElement : HTMLElement , options : Options ) : void {
32- const originalNode = node ;
33- const isPseudo = node . nodeType ( ) === Node . ELEMENT_NODE && node . pseudoType ( ) ;
34- if ( isPseudo && node . parentNode ) {
35- node = node . parentNode ;
36- }
37-
38- // Special case rendering the node links for view transition pseudo elements.
39- // We don't include the ancestor name in the node link because
40- // they always have the same ancestor. See crbug.com/340633630.
41- if ( node . isViewTransitionPseudoNode ( ) ) {
42- const pseudoElement = parentElement . createChild ( 'span' , 'extra node-label-pseudo' ) ;
43- const viewTransitionPseudoText = `::${ originalNode . pseudoType ( ) } (${ originalNode . pseudoIdentifier ( ) } )` ;
44- UI . UIUtils . createTextChild ( pseudoElement , viewTransitionPseudoText ) ;
45- UI . Tooltip . Tooltip . install ( parentElement , options . tooltip || viewTransitionPseudoText ) ;
46- return ;
47- }
48-
49- const nameElement = parentElement . createChild ( 'span' , 'node-label-name' ) ;
50- if ( options . textContent ) {
51- nameElement . textContent = options . textContent ;
52- UI . Tooltip . Tooltip . install ( parentElement , options . tooltip || options . textContent ) ;
53- return ;
54- }
55-
56- let title = node . nodeNameInCorrectCase ( ) ;
57- nameElement . textContent = title ;
58-
59- const idAttribute = node . getAttribute ( 'id' ) ;
60- if ( idAttribute ) {
61- const idElement = parentElement . createChild ( 'span' , 'node-label-id' ) ;
62- const part = '#' + idAttribute ;
63- title += part ;
64- UI . UIUtils . createTextChild ( idElement , part ) ;
65-
66- // Mark the name as extra, since the ID is more important.
67- nameElement . classList . add ( 'extra' ) ;
68- }
69-
70- const classAttribute = node . getAttribute ( 'class' ) ;
71- if ( classAttribute ) {
72- const classes = classAttribute . split ( / \s + / ) ;
73- if ( classes . length ) {
74- const foundClasses = new Set < string > ( ) ;
75- const classesElement = parentElement . createChild ( 'span' , 'extra node-label-class' ) ;
76- for ( let i = 0 ; i < classes . length ; ++ i ) {
77- const className = classes [ i ] ;
78- if ( className && ! options . hiddenClassList ?. includes ( className ) && ! foundClasses . has ( className ) ) {
79- const part = '.' + className ;
80- title += part ;
81- UI . UIUtils . createTextChild ( classesElement , part ) ;
82- foundClasses . add ( className ) ;
83- }
84- }
85- }
86- }
87-
88- if ( isPseudo ) {
89- const pseudoIdentifier = originalNode . pseudoIdentifier ( ) ;
90- const pseudoElement = parentElement . createChild ( 'span' , 'extra node-label-pseudo' ) ;
91- let pseudoText = '::' + originalNode . pseudoType ( ) ;
92- if ( pseudoIdentifier ) {
93- pseudoText += `(${ pseudoIdentifier } )` ;
94- }
32+ interface ViewInput {
33+ dynamic ?: boolean ;
34+ disabled ?: boolean ;
35+ preventKeyboardFocus ?: boolean ;
36+ tagName ?: string ;
37+ id ?: string ;
38+ classes : string [ ] ;
39+ pseudo ?: string ;
40+ onClick : ( ) => void ;
41+ onMouseOver : ( ) => void ;
42+ onMouseLeave : ( ) => void ;
43+ }
9544
96- UI . UIUtils . createTextChild ( pseudoElement , pseudoText ) ;
97- title += pseudoText ;
98- }
99- UI . Tooltip . Tooltip . install ( parentElement , options . tooltip || title ) ;
45+ export type View = ( input : ViewInput , output : object , target : HTMLElement ) => void ;
46+
47+ const DEFAULT_VIEW : View = ( input , _output , target : HTMLElement ) => {
48+ // clang-format off
49+ render ( html `${ ( input . tagName || input . pseudo ) ?
50+ html `< style > ${ domLinkifierStyles } </ style
51+ > < span class ="monospace "
52+ > < button class ="node-link text-button link-style ${ classMap ( {
53+ 'dynamic-link' : Boolean ( input . dynamic ) ,
54+ disabled : Boolean ( input . disabled )
55+ } ) } "
56+ jslog =${ VisualLogging . link ( 'node' ) . track ( { click : true , keydown : 'Enter' } ) }
57+ tabindex =${ input . preventKeyboardFocus ? - 1 : 0 }
58+ @click=${ input . onClick }
59+ @mouseover=${ input . onMouseOver }
60+ @mouseleave=${ input . onMouseLeave }
61+ title=${ [
62+ input . tagName ?? '' ,
63+ input . id ? `#${ input . id } ` : '' ,
64+ ...input . classes . map ( c => `.${ c } ` ) ,
65+ input . pseudo ? `::${ input . pseudo } ` : '' ,
66+ ] . join ( ' ' ) } > ${
67+ [
68+ input . tagName ? html `< span class ="node-label-name "> ${ input . tagName } </ span > ` : nothing ,
69+ input . id ? html `< span class ="node-label-id "> #${ input . id } </ span > ` : nothing ,
70+ ...input . classes . map ( className => html `< span class ="extra node-label-class "> .${ className } </ span > ` ) ,
71+ input . pseudo ? html `< span class ="extra node-label-pseudo "> ${ input . pseudo } </ span > ` : nothing ,
72+ ]
73+ } </ button
74+ > </ span > ` : i18nString ( UIStrings . node ) } `, target , { host : input } ) ;
75+ // clang-format on
10076} ;
10177
10278export class DOMNodeLink extends UI . Widget . Widget {
10379 #node: SDK . DOMModel . DOMNode | undefined = undefined ;
10480 #options: Options | undefined = undefined ;
81+ #view: View ;
10582
106- constructor ( element ?: HTMLElement , node ?: SDK . DOMModel . DOMNode , options ?: Options ) {
83+ constructor ( element ?: HTMLElement , node ?: SDK . DOMModel . DOMNode , options ?: Options , view = DEFAULT_VIEW ) {
10784 super ( true , undefined , element ) ;
10885 this . element . classList . remove ( 'vbox' ) ;
10986 this . #node = node ;
11087 this . #options = options ;
88+ this . #view = view ;
11189 this . performUpdate ( ) ;
11290 }
11391
11492 override performUpdate ( ) : void {
115- const node = this . #node;
11693 const options = this . #options ?? {
11794 tooltip : undefined ,
11895 preventKeyboardFocus : undefined ,
11996 textContent : undefined ,
12097 isDynamicLink : false ,
12198 disabled : false ,
12299 } ;
123- this . contentElement . removeChildren ( ) ;
124- if ( ! node ) {
125- this . contentElement . appendChild ( document . createTextNode ( i18nString ( UIStrings . node ) ) ) ;
100+ const viewInput : ViewInput = {
101+ dynamic : options . isDynamicLink ,
102+ disabled : options . disabled ,
103+ preventKeyboardFocus : options . preventKeyboardFocus ,
104+ classes : [ ] ,
105+ onClick : ( ) => {
106+ void Common . Revealer . reveal ( this . #node) ;
107+ return false ;
108+ } ,
109+ onMouseOver : ( ) => {
110+ this . #node?. highlight ?.( ) ;
111+ } ,
112+ onMouseLeave : ( ) => {
113+ SDK . OverlayModel . OverlayModel . hideDOMNodeHighlight ( ) ;
114+ } ,
115+ } ;
116+ if ( ! this . #node) {
117+ this . #view( viewInput , { } , this . contentElement ) ;
126118 return ;
127119 }
128120
129- const root = this . contentElement . createChild ( 'span' , 'monospace' ) ;
130- this . registerRequiredCSS ( domLinkifierStyles ) ;
131- const link = root . createChild ( 'button' , 'node-link text-button link-style' ) ;
132- link . classList . toggle ( 'dynamic-link' , options . isDynamicLink ) ;
133- link . classList . toggle ( 'disabled' , options . disabled ) ;
134- link . setAttribute ( 'jslog' , `${ VisualLogging . link ( 'node' ) . track ( { click : true , keydown : 'Enter' } ) } ` ) ;
121+ let node = this . #node;
122+ const isPseudo = node . nodeType ( ) === Node . ELEMENT_NODE && node . pseudoType ( ) ;
123+ if ( isPseudo && node . parentNode ) {
124+ node = node . parentNode ;
125+ }
135126
136- decorateNodeLabel ( node , link , options ) ;
127+ // Special case rendering the node links for view transition pseudo elements.
128+ // We don't include the ancestor name in the node link because
129+ // they always have the same ancestor. See crbug.com/340633630.
130+ if ( node . isViewTransitionPseudoNode ( ) ) {
131+ viewInput . pseudo = `::${ this . #node. pseudoType ( ) } (${ this . #node. pseudoIdentifier ( ) } )` ;
132+ this . #view( viewInput , { } , this . contentElement ) ;
133+ return ;
134+ }
135+
136+ if ( options . textContent ) {
137+ viewInput . tagName = options . textContent ;
138+ this . #view( viewInput , { } , this . contentElement ) ;
139+ return ;
140+ }
137141
138- link . addEventListener ( 'click' , ( ) => {
139- void Common . Revealer . reveal ( node , false ) ;
140- return false ;
141- } , false ) ;
142- link . addEventListener ( 'mouseover' , node . highlight . bind ( node , undefined ) , false ) ;
143- link . addEventListener ( 'mouseleave' , ( ) => SDK . OverlayModel . OverlayModel . hideDOMNodeHighlight ( ) , false ) ;
142+ viewInput . tagName = node . nodeNameInCorrectCase ( ) ;
144143
145- if ( options . preventKeyboardFocus ) {
146- link . tabIndex = - 1 ;
144+ const idAttribute = node . getAttribute ( 'id' ) ;
145+ if ( idAttribute ) {
146+ viewInput . id = idAttribute ;
147147 }
148+
149+ const classAttribute = node . getAttribute ( 'class' ) ;
150+ if ( classAttribute ) {
151+ const classes = classAttribute . split ( / \s + / ) ;
152+ if ( classes . length ) {
153+ const foundClasses = new Set < string > ( ) ;
154+ for ( let i = 0 ; i < classes . length ; ++ i ) {
155+ const className = classes [ i ] ;
156+ if ( className && ! options . hiddenClassList ?. includes ( className ) && ! foundClasses . has ( className ) ) {
157+ foundClasses . add ( className ) ;
158+ }
159+ }
160+ viewInput . classes = [ ...foundClasses ] ;
161+ }
162+ }
163+ if ( isPseudo ) {
164+ const pseudoIdentifier = this . #node. pseudoIdentifier ( ) ;
165+ let pseudoText = '::' + this . #node. pseudoType ( ) ;
166+ if ( pseudoIdentifier ) {
167+ pseudoText += `(${ pseudoIdentifier } )` ;
168+ }
169+ viewInput . pseudo = pseudoText ;
170+ }
171+ this . #view( viewInput , { } , this . contentElement ) ;
148172 }
149173}
150174
175+ interface DeferredViewInput {
176+ preventKeyboardFocus ?: boolean ;
177+ onClick : ( ) => void ;
178+ }
179+
180+ type DeferredView = ( input : DeferredViewInput , output : object , target : HTMLElement ) => void ;
181+
182+ const DEFERRED_DEFAULT_VIEW : DeferredView = ( input , _output , target : HTMLElement ) => {
183+ // clang-format off
184+ render ( html `
185+ < style > ${ domLinkifierStyles } </ style >
186+ < button class ="node-link text-button link-style "
187+ jslog =${ VisualLogging . link ( 'node' ) . track ( { click : true } ) }
188+ tabindex =${ input . preventKeyboardFocus ? - 1 : 0 }
189+ @click=${ input . onClick }
190+ @mousedown=${ ( e : Event ) => e . consume ( ) } >
191+ < slot > </ slot >
192+ </ button > ` , target , { host : input } ) ;
193+ // clang-format on
194+ } ;
195+
151196export class DeferredDOMNodeLink extends UI . Widget . Widget {
152197 #deferredNode: SDK . DOMModel . DeferredDOMNode | undefined = undefined ;
153198 #options: Options | undefined = undefined ;
199+ #view: DeferredView ;
154200
155- constructor ( element ?: HTMLElement , deferredNode ?: SDK . DOMModel . DeferredDOMNode , options ?: Options ) {
201+ constructor (
202+ element ?: HTMLElement , deferredNode ?: SDK . DOMModel . DeferredDOMNode , options ?: Options ,
203+ view : DeferredView = DEFERRED_DEFAULT_VIEW ) {
156204 super ( true , undefined , element ) ;
157205 this . element . classList . remove ( 'vbox' ) ;
158206 this . #deferredNode = deferredNode ;
159207 this . #options = options ;
208+ this . #view = view ;
160209 this . performUpdate ( ) ;
161210 }
162211
163212 override performUpdate ( ) : void {
164- this . contentElement . removeChildren ( ) ;
165- const deferredNode = this . #deferredNode;
166- if ( ! deferredNode ) {
167- return ;
168- }
169- const options = this . #options ?? {
170- tooltip : undefined ,
171- preventKeyboardFocus : undefined ,
213+ const viewInput = {
214+ preventKeyboardFocus : this . #options?. preventKeyboardFocus ,
215+ onClick : ( ) => {
216+ this . #deferredNode?. resolve ?.( node => {
217+ void Common . Revealer . reveal ( node ) ;
218+ } ) ;
219+ } ,
172220 } ;
173- this . registerRequiredCSS ( domLinkifierStyles ) ;
174- const link = this . contentElement . createChild ( 'button' , 'node-link text-button link-style' ) ;
175- link . setAttribute ( 'jslog' , `${ VisualLogging . link ( 'node' ) . track ( { click : true } ) } ` ) ;
176- link . createChild ( 'slot' ) ;
177- link . addEventListener ( 'click' , deferredNode . resolve . bind ( deferredNode , onDeferredNodeResolved ) , false ) ;
178- link . addEventListener ( 'mousedown' , e => e . consume ( ) , false ) ;
179-
180- if ( options . preventKeyboardFocus ) {
181- link . tabIndex = - 1 ;
182- }
183-
184- function onDeferredNodeResolved ( node : SDK . DOMModel . DOMNode | null ) : void {
185- void Common . Revealer . reveal ( node ) ;
186- }
221+ this . #view( viewInput , { } , this . contentElement ) ;
187222 }
188223}
189224
0 commit comments