@@ -3,24 +3,22 @@ import type {
33 CustomElementDeclaration ,
44 Declaration ,
55 Package ,
6+ Slot ,
67} from 'custom-elements-manifest' ;
78
89import { LitElement , css , html , type PropertyValues } from 'lit' ;
910import { customElement } from 'lit/decorators/custom-element.js' ;
1011import { property } from 'lit/decorators/property.js' ;
1112import { ifDefined } from 'lit/directives/if-defined.js' ;
1213
13- type KnobRenderer < T > = (
14- this : PftElementKnobs < HTMLElement > ,
15- member : T ,
16- info : T extends Attribute ? AttributeKnobInfo : KnobInfo ,
17- ) => unknown ;
14+ import 'zero-md' ;
1815
19- interface KnobInfo {
16+ interface KnobInfo < E > {
17+ element : E ;
2018 knobId : string ;
2119}
2220
23- interface AttributeKnobInfo extends KnobInfo {
21+ interface AttributeKnobInfo < E > extends KnobInfo < E > {
2422 isBoolean : boolean ;
2523 isEnum : boolean ;
2624 isNullable : boolean ;
@@ -29,7 +27,25 @@ interface AttributeKnobInfo extends KnobInfo {
2927 values : string [ ] ;
3028}
3129
32- export type AttributeRenderer = KnobRenderer < Attribute > ;
30+ type ContentKnobInfo < E > = KnobInfo < E > ;
31+
32+ type KnobRenderer < T , E extends HTMLElement = HTMLElement > = (
33+ this : PftElementKnobs < E > ,
34+ member : T ,
35+ info :
36+ T extends Attribute ? AttributeKnobInfo < E >
37+ : T extends Slot [ ] ? ContentKnobInfo < E >
38+ : KnobInfo < E > ,
39+ ) => unknown ;
40+
41+ export type AttributeRenderer < E extends HTMLElement > = KnobRenderer < Attribute , E > ;
42+ export type ContentRenderer < E extends HTMLElement > = KnobRenderer < Slot [ ] , E > ;
43+
44+ const isCheckable = ( el : HTMLElement ) : el is HTMLElement & { checked : boolean } =>
45+ 'checked' in el ;
46+
47+ const isValue = ( el : HTMLElement ) : el is HTMLElement & { value : string } =>
48+ 'value' in el ;
3349
3450const isCustomElementDecl = ( decl : Declaration ) : decl is CustomElementDeclaration =>
3551 'customElement' in decl ;
@@ -41,11 +57,18 @@ export class PftElementKnobs<T extends HTMLElement> extends LitElement {
4157 #element {
4258 padding: 1em;
4359 }
44- fieldset {
60+
61+ dl#slot-descriptions {
4562 display: grid;
46- gap: 4px;
47- grid-template-columns: max-content 1fr;
48- align-items: center;
63+ gap: 8px;
64+ grid-template-columns: max-content auto;
65+ & dd {
66+ margin-inline-start: 0;
67+ }
68+ }
69+
70+ pf-text-area {
71+ width: 100%;
4972 }
5073 ` ,
5174 ] ;
@@ -56,7 +79,9 @@ export class PftElementKnobs<T extends HTMLElement> extends LitElement {
5679
5780 @property ( { attribute : false } ) element : T | null = null ;
5881
59- @property ( { attribute : false } ) renderAttribute : AttributeRenderer = this . #renderAttribute;
82+ @property ( { attribute : false } ) renderAttribute : AttributeRenderer < T > = this . #renderAttribute;
83+
84+ @property ( { attribute : false } ) renderContent : ContentRenderer < T > = this . #renderContent;
6085
6186 #mo = new MutationObserver ( this . #loadTemplate) ;
6287
@@ -101,7 +126,7 @@ export class PftElementKnobs<T extends HTMLElement> extends LitElement {
101126 }
102127 }
103128
104- #getAttributeInfo ( attribute : Attribute ) : AttributeKnobInfo {
129+ #getInfoForAttribute ( attribute : Attribute , element : T ) : AttributeKnobInfo < T > {
105130 // NOTE: we assume typescript types
106131 const type = attribute ?. type ?. text ?? '' ;
107132 const isUnion = ! ! type . includes ?.( '|' ) ;
@@ -115,6 +140,7 @@ export class PftElementKnobs<T extends HTMLElement> extends LitElement {
115140 const knobId = `knob-attribute-${ attribute . name } ` ;
116141 return {
117142 knobId,
143+ element,
118144 isBoolean,
119145 isEnum,
120146 isNullable,
@@ -124,55 +150,96 @@ export class PftElementKnobs<T extends HTMLElement> extends LitElement {
124150 } ;
125151 }
126152
127- #renderAttribute( attribute : Attribute , info : AttributeKnobInfo ) {
128- const { knobId, isEnum, isBoolean, values } = info ;
153+ #renderAttribute( attribute : Attribute , info : AttributeKnobInfo < T > ) {
154+ const { knobId, element , isEnum, isBoolean, isNumber , values } = info ;
129155 const QUOTE_RE = / ^ [ ' " ] ( .* ) [ ' " ] $ / ;
156+ const attributeValue =
157+ element ?. getAttribute ( attribute . name )
158+ ?? attribute . default ?. replace ( QUOTE_RE , '$1' ) ;
130159 return html `
131- < label for ="${ knobId } "> ${ attribute . name } </ label > ${ isBoolean ? html `
132- < input id ="${ knobId } "
133- type ="checkbox "
134- ?checked ="${ attribute . default === 'true' } "
135- data-attribute ="${ attribute . name } "> ` : isEnum ? html `
160+ < h3 > < code > ${ attribute . name } </ code > </ h3 >
161+ < zero-md > < script type ="text/markdown "> ${ attribute . summary ?? '' } </ script > </ zero-md >
162+ < zero-md > < script type ="text/markdown "> ${ attribute . description ?? '' } </ script > </ zero-md >
163+ ${ isBoolean ? html `
164+ < label for ="${ knobId } "> Present</ label >
165+ < pf-switch id ="${ knobId } "
166+ ?checked ="${ attribute . default === 'true' } "
167+ data-attribute ="${ attribute . name } "> </ pf-switch > ` : isEnum ? html `
136168 < pf-select id ="${ knobId } "
137169 placeholder ="Select a value "
170+ aria-label ="Value "
138171 data-attribute ="${ attribute . name } "
139- value ="${ ifDefined ( attribute . default ?. replace ( QUOTE_RE , '$1' ) ) } "> ${ values ! . map ( x => html `
172+ value ="${ ifDefined ( attributeValue ) } "> ${ values ! . map ( x => html `
140173 < pf-option > ${ x . trim ( ) . replace ( QUOTE_RE , '$1' ) } </ pf-option > ` ) }
141- </ pf-select >
142- ` : html `
174+ </ pf-select > ` : html `
143175 < pf-text-input id ="${ knobId } "
144- value ="${ ifDefined ( attribute . default ?. replace ( QUOTE_RE , '$1' ) ) } "
176+ aria-label ="Value "
177+ value ="${ ifDefined ( attributeValue ) } "
178+ type ="${ ifDefined ( isNumber ? 'number' : undefined ) } "
145179 helper-text ="${ ifDefined ( attribute . type ?. text ) } "
146180 data-attribute ="${ attribute . name } "> </ pf-text-input > ` }
147181 ` ;
148182 }
149183
184+ #renderContent( slots : Slot [ ] , info : ContentKnobInfo < T > ) {
185+ // todo : change listener is inflexible
186+ return html `
187+ < dl id ="slot-descriptions "> ${ slots . map ( x => html `
188+ < dt > ${ x . name ? html `
189+ < code > ${ x . name } </ code > ` : html `
190+ < strong > Default slot</ strong > ` }
191+ </ dt >
192+ < dd >
193+ < zero-md > < script type ="text/markdown "> ${ x . summary ?? '' } </ script > </ zero-md >
194+ < zero-md > < script type ="text/markdown "> ${ x . description ?? '' } </ script > </ zero-md >
195+ </ dd > ` ) }
196+ </ dl >
197+ < pf-text-area id ="${ info . knobId } "
198+ resize auto-resize
199+ aria-label ="HTML Content "
200+ @input ="${ this . #onKnobChangedContent} "
201+ .value ="${ info . element . innerHTML } "> </ pf-text-area >
202+ ` ;
203+ }
204+
150205 #renderKnobs( ) {
151206 const decl = this . #elementDecl;
152207 const { element, tag, manifest } = this ;
153208 if ( element && decl && tag && manifest ) {
154- const { attributes } = decl ;
155-
156- const onChange = ( e : Event & { target : HTMLInputElement } ) => {
157- if ( e . target instanceof HTMLInputElement && e . target . type === 'checkbox' ) {
158- this . element ?. toggleAttribute ( e . target . dataset . attribute ! , e . target . checked ) ;
159- } else {
160- this . element ?. setAttribute ( e . target . dataset . attribute ! , e . target . value ) ;
161- }
162- } ;
163-
209+ const { attributes, slots } = decl ;
164210 return html `
165211 < form @submit ="${ ( e : Event ) => e . preventDefault ( ) } ">
166212 ${ ! attributes ? '' : html `
167- < fieldset @change ="${ onChange } " @input ="${ onChange } ">
168- < legend > Attributes</ legend >
169- ${ attributes . map ( attr => this . renderAttribute ( attr , this . #getAttributeInfo( attr ) ) ) }
170- </ fieldset > ` }
213+ < section id ="attributes "
214+ @change ="${ this . #onKnobChangeAttribute} "
215+ @input ="${ this . #onKnobChangeAttribute} ">
216+ < h2 > Attributes</ h2 >
217+ ${ attributes . map ( x => this . renderAttribute ( x , this . #getInfoForAttribute( x , element ) ) ) }
218+ </ section > ` }
219+ ${ ! slots ? '' : html `
220+ < section >
221+ < h2 > Slots</ h2 >
222+ ${ this . renderContent ( slots , { knobId : 'knob-html-content' , element } ) }
223+ </ section > ` }
171224 </ form >
172225 ` ;
173226 }
174227 }
175228
229+ #onKnobChangeAttribute( e : Event & { target : HTMLElement } ) {
230+ if ( isCheckable ( e . target ) ) {
231+ this . element ?. toggleAttribute ( e . target . dataset . attribute ! , e . target . checked ) ;
232+ } else if ( isValue ( e . target ) ) {
233+ this . element ?. setAttribute ( e . target . dataset . attribute ! , e . target . value ) ;
234+ }
235+ }
236+
237+ #onKnobChangedContent( e : Event & { target : HTMLInputElement } ) {
238+ if ( this . element ) {
239+ this . element . innerHTML = e . target . value ;
240+ }
241+ }
242+
176243 protected override render ( ) : unknown {
177244 return html `
178245 < div id ="element "> ${ this . #node ?? '' } </ div >
0 commit comments