@@ -156,20 +156,22 @@ function renderResults(entries, query) {
156156 const specInfo = metadata . specs [ entry . status ] [ entry . spec ] ;
157157 const link = new URL ( entry . uri , specInfo . url ) . href ;
158158 const title = escapeHTML ( specInfo . title ) ;
159- const cite = metadata . types . idl . has ( entry . type )
159+ const cites = metadata . types . idl . has ( entry . type )
160160 ? howToCiteIDL ( citeTerm , entry , overloadedPairs )
161161 : metadata . types . markup . has ( entry . type )
162162 ? howToCiteMarkup ( citeTerm , entry )
163163 : metadata . types . css . has ( entry . type ) ||
164164 metadata . types . http . has ( entry . type )
165165 ? howToCiteAnchor ( citeTerm , entry )
166166 : howToCiteTerm ( citeTerm , entry ) ;
167+ // Each citation is its own button: click/tap the citation to copy just it.
168+ const citeCell = cites . map ( citeButton ) . join ( '<br>' ) ;
167169 let row = `
168170 <tr>
169171 <td><a href="${ link } ">${ title } </a></td>
170172 <td>${ entry . shortname } </td>
171173 <td>${ entry . type } </td>
172- <td>${ cite } </td>
174+ <td>${ citeCell } </td>
173175 </tr>` ;
174176 html += row ;
175177 }
@@ -221,19 +223,17 @@ function howToCiteIDL(term, entry, overloadedPairs = null) {
221223 const { type, for : forList } = entry ;
222224 const safeTerm = escapeHTML ( term ) ;
223225 if ( forList ) {
224- return forList
225- . map ( f => {
226- const safeF = escapeHTML ( f ) ;
227- let displayTerm = type === 'enum-value' ? `"${ safeTerm } "` : safeTerm ;
228- if ( overloadedPairs ?. has ( `${ entry . uri } |${ f } ` ) ) {
229- const hint = extractOverloadHint ( entry . uri , f , term ) ;
230- if ( hint ) {
231- displayTerm = displayTerm . replace ( '()' , `(${ escapeHTML ( hint ) } )` ) ;
232- }
226+ return forList . map ( f => {
227+ const safeF = escapeHTML ( f ) ;
228+ let displayTerm = type === 'enum-value' ? `"${ safeTerm } "` : safeTerm ;
229+ if ( overloadedPairs ?. has ( `${ entry . uri } |${ f } ` ) ) {
230+ const hint = extractOverloadHint ( entry . uri , f , term ) ;
231+ if ( hint ) {
232+ displayTerm = displayTerm . replace ( '()' , `(${ escapeHTML ( hint ) } )` ) ;
233233 }
234- return `{{ ${ safeF } / ${ displayTerm ? displayTerm : '""' } }}` ;
235- } )
236- . join ( '<br>' ) ;
234+ }
235+ return `{{ ${ safeF } / ${ displayTerm ? displayTerm : '""' } }}` ;
236+ } ) ;
237237 }
238238 let cite ;
239239 switch ( type ) {
@@ -251,7 +251,7 @@ function howToCiteIDL(term, entry, overloadedPairs = null) {
251251 cite = cite . replace ( '()' , `(${ escapeHTML ( hint ) } )` ) ;
252252 }
253253 }
254- return cite ;
254+ return [ cite ] ;
255255}
256256
257257/**
@@ -297,45 +297,96 @@ function extractOverloadHint(uri, forContext, term) {
297297}
298298
299299function howToCiteMarkup ( term , entry ) {
300- const { type, for : forList , shortname } = entry ;
300+ const { type, for : forList } = entry ;
301301 const safeTerm = escapeHTML ( term ) ;
302302 if ( forList ) {
303- return forList . map ( f => `[^${ escapeHTML ( f ) } /${ safeTerm } ^]` ) . join ( '<br>' ) ;
303+ return forList . map ( f => `[^${ escapeHTML ( f ) } /${ safeTerm } ^]` ) ;
304304 }
305305 if ( type === 'element-attr' ) {
306- return `[^/${ safeTerm } ^]` ;
306+ return [ `[^/${ safeTerm } ^]` ] ;
307307 }
308- return `[^${ safeTerm } ^]` ;
308+ return [ `[^${ safeTerm } ^]` ] ;
309309}
310310
311311function howToCiteAnchor ( term , entry ) {
312312 const { type, for : forList } = entry ;
313313 term = escapeHTML ( term ) ;
314314 if ( ! forList ) {
315- return escapeHTML ( `<a data-xref-type="${ type } ">${ term } </a>` ) ;
315+ return [ escapeHTML ( `<a data-xref-type="${ type } ">${ term } </a>` ) ] ;
316316 }
317- return forList
318- . map ( f =>
319- escapeHTML (
320- `<a data-xref-type="${ type } " data-xref-for="${ f } ">${ term } </a>` ,
321- ) ,
322- )
323- . join ( '<br>' ) ;
317+ return forList . map ( f =>
318+ escapeHTML ( `<a data-xref-type="${ type } " data-xref-for="${ f } ">${ term } </a>` ) ,
319+ ) ;
324320}
325321
326322function howToCiteTerm ( term , entry ) {
327- const { type, for : forList , shortname } = entry ;
323+ const { type, for : forList } = entry ;
328324 term = escapeHTML ( term . replace ( '/' , '\\/' ) ) ;
329325 if ( forList ) {
330- return forList . map ( f => `[=${ escapeHTML ( f ) } /${ term } =]` ) . join ( '<br>' ) ;
326+ return forList . map ( f => `[=${ escapeHTML ( f ) } /${ term } =]` ) ;
331327 }
332- return `[=${ term } =]` ;
328+ return [ `[=${ term } =]` ] ;
333329}
334330
335331function escapeHTML ( str ) {
336332 return str . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
337333}
338334
335+ // Render one citation as its own copy button. The accessible name conveys the
336+ // action + value; `cite` is already escaped for &<>, but an attribute value
337+ // also needs " escaped (e.g. exception citations like {{"DOMException"}}).
338+ function citeButton ( cite ) {
339+ const label = `Copy citation ${ cite . replace ( / " / g, '"' ) } ` ;
340+ return `<button type="button" class="cite" aria-label="${ label } ">${ cite } </button>` ;
341+ }
342+
343+ // A single reused live region. Reusing one element (rather than appending a
344+ // new toast per click) avoids stacking overlapping toasts and competing
345+ // announcements when copying several citations in quick succession.
346+ let copyToast ;
347+ let copyToastTimer ;
348+ function showCopyToast ( message ) {
349+ if ( ! copyToast ) {
350+ copyToast = document . createElement ( 'div' ) ;
351+ copyToast . className = 'copy-toast' ;
352+ copyToast . setAttribute ( 'role' , 'status' ) ;
353+ document . body . appendChild ( copyToast ) ;
354+ }
355+ clearTimeout ( copyToastTimer ) ;
356+ // Set the text on the next frame so the (already-attached) live region
357+ // announces it as a change rather than as initial content.
358+ requestAnimationFrame ( ( ) => {
359+ copyToast . textContent = message ;
360+ copyToast . classList . add ( 'is-visible' ) ;
361+ } ) ;
362+ copyToastTimer = setTimeout ( ( ) => {
363+ copyToast . classList . remove ( 'is-visible' ) ;
364+ } , 1500 ) ;
365+ }
366+
367+ async function copyCitation ( text ) {
368+ if ( ! text ) return ;
369+ if ( ! navigator . clipboard ) {
370+ showCopyToast ( 'Clipboard unavailable' ) ;
371+ return ;
372+ }
373+ try {
374+ await navigator . clipboard . writeText ( text ) ;
375+ showCopyToast ( `Copied: ${ text } ` ) ;
376+ } catch ( err ) {
377+ console . error ( err ) ;
378+ showCopyToast ( 'Copy failed' ) ;
379+ }
380+ }
381+
382+ // Click-to-copy: each citation is itself a <button>, so clicking/tapping the
383+ // citation copies just that one. Being a real button, keyboard activation
384+ // (Enter/Space) dispatches a click for free.
385+ output . addEventListener ( 'click' , e => {
386+ const cite = e . target . closest ( '.cite' ) ;
387+ if ( cite ) copyCitation ( cite . textContent ) ;
388+ } ) ;
389+
339390async function ready ( ) {
340391 const updateInput = ( el , values ) => {
341392 el . setAttribute ( 'placeholder' , values . slice ( 0 , 5 ) . join ( ',' ) ) ;
0 commit comments