11import { LiveStore , NamedNode , st , sym } from 'rdflib'
22import { ns } from 'solid-ui'
3- import { closeIcon , editIcon } from './icons'
3+ import 'solid-ui/components/actions/button'
4+ import { closeIcon , editIcon , plusIcon , trashIcon } from './icons'
45/* The following code was generated by AI Model: GPT-5.4
56 Prompt: can you create me a form to enter this data [
67ns.foaf('homepage'),
@@ -19,7 +20,7 @@ type ProfileLinkField = {
1920 predicate : NamedNode
2021}
2122
22- type ProfileLinksValues = Record < ProfileLinkField [ 'key' ] , string >
23+ type ProfileLinksValues = Record < ProfileLinkField [ 'key' ] , string [ ] >
2324
2425type EditProfileDetailsButtonOptions = {
2526 dom : HTMLDocument
@@ -56,6 +57,15 @@ export const profileLinkFields: ProfileLinkField[] = [
5657 }
5758]
5859
60+ function createEmptyProfileLinkValues ( ) : ProfileLinksValues {
61+ return {
62+ homepage : [ '' ] ,
63+ weblog : [ '' ] ,
64+ workplaceHomepage : [ '' ] ,
65+ schoolHomepage : [ '' ]
66+ }
67+ }
68+
5969function readProfileLinkValues ( store : LiveStore , subject : NamedNode ) : ProfileLinksValues {
6070 const doc = subject . doc ( )
6171 return profileLinkFields . reduce ( ( result , field ) => {
@@ -64,20 +74,14 @@ function readProfileLinkValues (store: LiveStore, subject: NamedNode): ProfileLi
6474 . map ( ( statement ) => statement . object ?. value || '' )
6575 . filter ( Boolean )
6676
67- result [ field . key ] = values . join ( '\n' )
77+ result [ field . key ] = values . length ? values : [ '' ]
6878 return result
69- } , {
70- homepage : '' ,
71- weblog : '' ,
72- workplaceHomepage : '' ,
73- schoolHomepage : ''
74- } as ProfileLinksValues )
79+ } , createEmptyProfileLinkValues ( ) )
7580}
7681
77- function normalizeUrlList ( value : string , fieldLabel : string ) : string [ ] {
82+ function normalizeUrlList ( values : string [ ] , fieldLabel : string ) : string [ ] {
7883 const unique = new Set < string > ( )
79- const lines = value
80- . split ( '\n' )
84+ const lines = values
8185 . map ( ( entry ) => entry . trim ( ) )
8286 . filter ( Boolean )
8387
@@ -99,6 +103,14 @@ function normalizeUrlList (value: string, fieldLabel: string): string[] {
99103 return [ ...unique ]
100104}
101105
106+ function formatDisplayUrl ( value : string ) : string {
107+ let displayValue = value . replace ( / ^ h t t p s ? : \/ \/ / i, '' )
108+ if ( / ^ [ ^ / ? # ] + \/ $ / . test ( displayValue ) ) {
109+ displayValue = displayValue . slice ( 0 , - 1 )
110+ }
111+ return displayValue
112+ }
113+
102114async function saveProfileLinkValues (
103115 store : LiveStore ,
104116 subject : NamedNode ,
@@ -163,25 +175,10 @@ export function appendProfileLinks (
163175 for ( const uri of uris ) {
164176 if ( uri === previousUri ) continue
165177 previousUri = uri
166-
167- let label = field . label
168- if ( uris . length > 1 ) {
169- const schemeIndex = uri . indexOf ( '//' )
170- if ( schemeIndex > 0 ) {
171- let end = uri . indexOf ( '/' , schemeIndex + 2 )
172- const dottedEnd = uri . lastIndexOf ( '.' , end )
173- if ( dottedEnd > 0 ) end = dottedEnd
174- const hostLabel = uri . slice ( schemeIndex + 2 , end )
175- if ( hostLabel ) {
176- label = `${ hostLabel } ${ label } `
177- }
178- }
179- }
180-
181- const textNode = dom . createTextNode ( label )
182178 const anchor = dom . createElement ( 'a' )
183- anchor . appendChild ( textNode )
179+ anchor . textContent = formatDisplayUrl ( uri )
184180 anchor . setAttribute ( 'href' , uri )
181+ anchor . setAttribute ( 'title' , `${ field . label } : ${ uri } ` )
185182
186183 const item = dom . createElement ( 'div' )
187184 item . className = 'social-pane__link-button'
@@ -214,6 +211,10 @@ export function createEditProfileDetailsButton (
214211
215212 const openDialog = ( ) => {
216213 const initialValues = readProfileLinkValues ( store , subject )
214+ const formState = profileLinkFields . reduce ( ( result , field ) => {
215+ result [ field . key ] = [ ...initialValues [ field . key ] ]
216+ return result
217+ } , createEmptyProfileLinkValues ( ) )
217218 const overlay = dom . createElement ( 'div' )
218219 overlay . classList . add ( 'social-pane__dialog-backdrop' , 'flex-center' )
219220
@@ -233,57 +234,147 @@ export function createEditProfileDetailsButton (
233234 title . textContent = 'Edit profile links'
234235 dialogHeader . appendChild ( title )
235236
236- const closeIconButton = dom . createElement ( 'button' )
237+ const closeIconButton = dom . createElement ( 'solid-ui- button' )
237238 closeIconButton . type = 'button'
238239 closeIconButton . className = 'social-pane__dialog-close'
239240 closeIconButton . setAttribute ( 'aria-label' , 'Close dialog' )
240- closeIconButton . innerHTML = closeIcon
241+ closeIconButton . setAttribute ( 'title' , 'Close' )
242+ closeIconButton . variant = 'icon'
243+ closeIconButton . size = 'md'
244+ const closeIconButtonIcon = dom . createElement ( 'span' )
245+ closeIconButtonIcon . className = 'social-pane__dialog-close-icon'
246+ closeIconButtonIcon . setAttribute ( 'slot' , 'icon' )
247+ closeIconButtonIcon . setAttribute ( 'aria-hidden' , 'true' )
248+ closeIconButtonIcon . innerHTML = closeIcon
249+ closeIconButton . appendChild ( closeIconButtonIcon )
241250 dialogHeader . appendChild ( closeIconButton )
242251
243252 const form = dom . createElement ( 'form' )
244253 form . classList . add ( 'social-pane__dialog-form' , 'flex-column' )
245254 dialog . appendChild ( form )
246255
256+ const fieldsContainer = dom . createElement ( 'div' )
257+ fieldsContainer . className = 'social-pane__dialog-fields'
258+ form . appendChild ( fieldsContainer )
259+
247260 const errorMessage = dom . createElement ( 'p' )
248261 errorMessage . className = 'social-pane__dialog-error'
249262
250- const fields = new Map < ProfileLinkField [ 'key' ] , HTMLInputElement > ( )
251- for ( const field of profileLinkFields ) {
252- const fieldWrapper = dom . createElement ( 'label' )
253- fieldWrapper . classList . add ( 'social-pane__dialog-field' , 'flex-column' )
254-
255- const fieldLabel = dom . createElement ( 'span' )
256- fieldLabel . className = 'social-pane__dialog-label'
257- fieldLabel . textContent = field . label
258- fieldWrapper . appendChild ( fieldLabel )
259-
260- const input = dom . createElement ( 'input' )
261- input . classList . add ( 'social-pane__dialog-input' , 'input' )
262- input . type = 'url'
263- input . placeholder = field . placeholder
264- input . value = initialValues [ field . key ] . split ( '\n' ) [ 0 ] || ''
265- fieldWrapper . appendChild ( input )
266-
267- fields . set ( field . key , input )
268- form . appendChild ( fieldWrapper )
263+ const focusRow = ( fieldKey : ProfileLinkField [ 'key' ] , rowIndex : number ) => {
264+ const nextField = fieldsContainer . querySelector < HTMLInputElement > ( `input[data-profile-link-key="${ fieldKey } "][data-profile-link-index="${ String ( rowIndex ) } "]` )
265+ nextField ?. focus ( )
266+ }
267+
268+ const renderFields = ( focusTarget ?: { fieldKey : ProfileLinkField [ 'key' ] , rowIndex : number } ) => {
269+ fieldsContainer . replaceChildren ( )
270+
271+ for ( const field of profileLinkFields ) {
272+ const fieldWrapper = dom . createElement ( 'section' )
273+ fieldWrapper . classList . add ( 'social-pane__dialog-field' , 'flex-column' )
274+
275+ const fieldHeader = dom . createElement ( 'div' )
276+ fieldHeader . className = 'social-pane__dialog-field-header'
277+ fieldWrapper . appendChild ( fieldHeader )
278+
279+ const fieldLabel = dom . createElement ( 'h3' )
280+ fieldLabel . className = 'social-pane__dialog-label'
281+ fieldLabel . textContent = field . label
282+ fieldHeader . appendChild ( fieldLabel )
283+
284+ const addButton = dom . createElement ( 'button' )
285+ addButton . type = 'button'
286+ addButton . className = 'social-pane__dialog-row-button social-pane__dialog-row-button--add'
287+ addButton . setAttribute ( 'aria-label' , `Add another ${ field . label . toLowerCase ( ) } ` )
288+ const addButtonIcon = dom . createElement ( 'span' )
289+ addButtonIcon . className = 'social-pane__dialog-row-button-icon social-pane__dialog-row-button-icon--add'
290+ addButtonIcon . setAttribute ( 'aria-hidden' , 'true' )
291+ addButtonIcon . innerHTML = plusIcon
292+ addButton . appendChild ( addButtonIcon )
293+ addButton . appendChild ( dom . createTextNode ( 'Add More' ) )
294+ addButton . addEventListener ( 'click' , ( ) => {
295+ formState [ field . key ] . push ( '' )
296+ renderFields ( { fieldKey : field . key , rowIndex : formState [ field . key ] . length - 1 } )
297+ } )
298+ fieldHeader . appendChild ( addButton )
299+
300+ const rows = dom . createElement ( 'div' )
301+ rows . className = 'social-pane__dialog-row-list'
302+ fieldWrapper . appendChild ( rows )
303+
304+ const values = formState [ field . key ] . length ? formState [ field . key ] : [ '' ]
305+ values . forEach ( ( value , index ) => {
306+ const row = dom . createElement ( 'div' )
307+ row . className = 'social-pane__dialog-row'
308+
309+ const input = dom . createElement ( 'input' )
310+ input . classList . add ( 'social-pane__dialog-input' , 'input' )
311+ input . type = 'url'
312+ input . placeholder = field . placeholder
313+ input . value = value
314+ input . setAttribute ( 'data-profile-link-key' , field . key )
315+ input . setAttribute ( 'data-profile-link-index' , String ( index ) )
316+ input . addEventListener ( 'input' , ( ) => {
317+ formState [ field . key ] [ index ] = input . value
318+ } )
319+ row . appendChild ( input )
320+
321+ const removeButton = dom . createElement ( 'solid-ui-button' )
322+ removeButton . type = 'button'
323+ removeButton . className = 'social-pane__dialog-row-button--remove'
324+ removeButton . setAttribute ( 'aria-label' , `Remove ${ field . label . toLowerCase ( ) } ${ index + 1 } ` )
325+ removeButton . setAttribute ( 'title' , 'Remove' )
326+ removeButton . variant = 'icon'
327+ removeButton . size = 'sm'
328+ const removeButtonIcon = dom . createElement ( 'span' )
329+ removeButtonIcon . className = 'social-pane__dialog-row-button-icon social-pane__dialog-row-button-icon--remove'
330+ removeButtonIcon . setAttribute ( 'slot' , 'icon' )
331+ removeButtonIcon . setAttribute ( 'aria-hidden' , 'true' )
332+ removeButtonIcon . innerHTML = trashIcon
333+ removeButton . appendChild ( removeButtonIcon )
334+ removeButton . addEventListener ( 'click' , ( ) => {
335+ if ( formState [ field . key ] . length === 1 ) {
336+ formState [ field . key ] [ 0 ] = ''
337+ } else {
338+ formState [ field . key ] . splice ( index , 1 )
339+ }
340+ renderFields ( )
341+ } )
342+ row . appendChild ( removeButton )
343+
344+ rows . appendChild ( row )
345+ } )
346+
347+ fieldsContainer . appendChild ( fieldWrapper )
348+ }
349+
350+ if ( focusTarget ) {
351+ focusRow ( focusTarget . fieldKey , focusTarget . rowIndex )
352+ }
269353 }
270354
355+ renderFields ( )
356+
271357 form . appendChild ( errorMessage )
272358
273359 const actions = dom . createElement ( 'div' )
274360 actions . className = 'social-pane__dialog-actions'
275361 form . appendChild ( actions )
276362
277- const closeButton = dom . createElement ( 'button' )
363+ const closeButton = dom . createElement ( 'solid-ui- button' )
278364 closeButton . type = 'button'
279- closeButton . className = 'social-pane__dialog-button btn-light'
280- closeButton . textContent = 'Close'
365+ closeButton . className = 'social-pane__dialog-button--cancel'
366+ closeButton . setAttribute ( 'aria-label' , 'Close dialog' )
367+ closeButton . label = 'Close'
368+ closeButton . variant = 'secondary'
369+ closeButton . size = 'md'
281370 actions . appendChild ( closeButton )
282371
283- const saveButton = dom . createElement ( 'button' )
372+ const saveButton = dom . createElement ( 'solid-ui- button' )
284373 saveButton . type = 'submit'
285- saveButton . className = 'social-pane__dialog-button btn-primary'
286- saveButton . textContent = 'Save Changes'
374+ saveButton . className = 'social-pane__dialog-button--save'
375+ saveButton . label = 'Save Changes'
376+ saveButton . variant = 'primary'
377+ saveButton . size = 'md'
287378 actions . appendChild ( saveButton )
288379
289380 const closeDialog = ( ) => {
@@ -310,14 +401,9 @@ export function createEditProfileDetailsButton (
310401 closeIconButton . disabled = true
311402
312403 const nextValues = profileLinkFields . reduce ( ( result , field ) => {
313- result [ field . key ] = fields . get ( field . key ) ?. value || ''
404+ result [ field . key ] = [ ... formState [ field . key ] ]
314405 return result
315- } , {
316- homepage : '' ,
317- weblog : '' ,
318- workplaceHomepage : '' ,
319- schoolHomepage : ''
320- } as ProfileLinksValues )
406+ } , createEmptyProfileLinkValues ( ) )
321407
322408 try {
323409 await saveProfileLinkValues ( store , subject , nextValues )
@@ -333,7 +419,7 @@ export function createEditProfileDetailsButton (
333419 } )
334420
335421 dom . body . appendChild ( overlay )
336- fields . get ( 'homepage' ) ?. focus ( )
422+ focusRow ( 'homepage' , 0 )
337423 }
338424
339425 button . addEventListener ( 'click' , openDialog )
0 commit comments